Software Design Reference System Architect · Contracts, Data, Architecture, Deployment

Lesson 0059 · System Architect · Module 3

Choose the Architecture Style from Drivers

The architecture style is a consequence of what the system must be — its drivers and quality characteristics — not a taste to declare. Order matters: characteristics first, style second.

Reading ≈ 10 minPractice ≈ 30 min

Mission tie-in: this is where the whole sequence converges — domain, state, invariants, components, contracts, and data all get a home. The style must serve them, not reshape them.

Knowledge: characteristics first, style second

Lesson 0043 turned vague goals into measurable architecture drivers. Now the payoff: the driver set constrains and informs the style. It rarely produces one mechanical answer, so compare credible alternatives and their costs. Asking "should we use microservices?" first hands the framing to fashion; asking "what must this system be?" first hands it to evidence.

Returns system · drivers → style
DriverWhat it demandsStyle response
One small team, one deployableLow coordination cost, simple ops.Modular monolith — modules, one process.
Policy changes weeklyFast, isolated change in one module.Module seam with enforced dependency rules.
Carrier and payments are externalTranslate at the edge; replace vendors.Ports and adapters at the boundary.
Compliance audit of decisionsAuditable facts, no accidental writes.Event or history stream inside the module.
The principle behind the principle

A style is a bundle of default answers to coordination questions, so choosing one is choosing what you want to be cheap and accepting what becomes expensive. Distribution has a single, precise effect underneath all the diagrams: it converts errors that were deterministic and in-process into partial failures that are not. In exchange you get independent failure, independent scaling, and independent release. Everything else people say about microservices is downstream of that trade.

The reason the modular monolith is the default for a small team is not modesty; it is option value. A module seam can become a service later, mechanically, if it was enforced. A service fleet cannot become modules later without a data-ownership merge project. Under uncertainty — and at the start you are uncertain about exactly the things that would justify distribution — you take the choice that is cheaper to reverse, and you keep the seam that preserves the option.

That argument inverts when the uncertainty is gone. If a regulator requires an isolated processing environment, if one component's load is two orders of magnitude larger than the rest, if two teams already exist and cannot coordinate a release, then delaying the boundary costs more than drawing it now — you are not preserving an option, you are deferring known work. And Conway's law sets an outer limit: if the organization will hand you four teams regardless of your diagram, one deployable that four teams edit is not a monolith, it is a queue.

For a small team, the modular monolith is the responsible default: one deployable, explicit modules, one composition root, and enforced dependency direction (lesson 0022). It keeps every seam from your boundary map (lesson 0053) while paying none of the distributed-system rent. A different style must earn its cost against a driver:

Every extraction pays permanent rent at the new process boundary: calls across it become network interactions (lesson 0031), while consistency boundaries should usually remain inside one service. Cross-service workflows now need explicit coordination, and versioning, observability, security, and operational ownership grow. Event-driven styles pay in another currency: temporal decoupling in exchange for asynchronous failure handling. The deciding question is never "which style is modern" but "which driver pays for the added rent."

Architecture styles are also measured after they are chosen. The dependency rules from lesson 0022 become fitness functions: automated checks that fail the build when a module imports across its seam, or a latency budget is breached. A style without fitness functions is a diagram; one with them is a contract.

# A fitness function: the boundary map as an enforced import rule.
# Fails the build when a module reaches outside its allowed set.
ALLOWED = {
    "return_journey": {"eligibility", "payments", "orders"},
    "eligibility": set(),  # policy is internal to this boundary
    "payments": set(),
}


def check_imports(graph: dict[str, set[str]]) -> list[str]:
    violations = []
    for module, imports in graph.items():
        for target in imports:
            if target not in ALLOWED[module]:
                violations.append(f"{module} must not import {target}")
    return violations

Worked: scoring the styles against the driver set

The architect did not begin with a style. They began with the five prioritized drivers from lesson 0043 and a rule for reading the table: a style earns a deployment boundary only where a driver pays for it. Anything else is rent with no tenant.

Driver-to-style decision table · returns pilot, six-person team
DriverModular monolithMonolith + outbox workerEligibility as a service
Availability: carrier down 30 min, 99% of labels within 15 min of recoveryFails — a synchronous carrier call ties the request to the vendorMeets it: intent persisted, worker retriesMeets it, but only because it also has a worker
Performance: 40 rps, eligibility decision p95 < 800 msMeets it with roomMeets it with roomMeets it; one network hop spends ~10–30 ms of the budget for nothing
Modifiability: one engineer ships a policy rule in a dayMeets itMeets itWorse: a rule that touches the contract needs two deploys, ordered
Operability: label failures > 2% for 5 min, disable within 10 minOne process to watch, one flagTwo processes, one flag; queue depth is a new signalThree processes, two on-call surfaces, six-person team
Security: object-level authorization on every readOne place to enforce itOne place to enforce itEnforced twice, or trusted across the wire

The table decides two things at once, and the second one is the interesting one. The monolith alone fails the availability driver, so the pilot does need a second deployable — the outbox worker recorded in ADR 0003 (lesson 0046). That is a real distributed system with a real cost: a queue to watch, retries to make idempotent, a pending state the customer can see. It was accepted because a named driver pays for it. Being “a modular monolith” was never the goal; not paying unjustified rent was.

Eligibility-as-a-service is the option worth rejecting carefully, because it is the one a team asks for. It is not slow — 800 ms is a generous budget for one hop. It fails on the other three rows: it makes the most frequent change in the system (a weekly policy rule) more expensive, adds an on-call surface to a team of six with a rotation of three, and splits a consistency boundary that lesson 0055 put together. No driver asks for independent scaling of eligibility at 40 rps, and no second team exists to own it.

# The rule the table encodes: every extra deployable needs a paying driver.
STYLE_DEPLOYABLES = {
    "modular_monolith": ["web"],
    "monolith_plus_worker": ["web", "outbox_worker"],
    "eligibility_service": ["web", "outbox_worker", "eligibility"],
}

# Which driver pays for each boundary beyond the first?
PAYING_DRIVER = {
    "outbox_worker": "availability: carrier outage must not lose eligible requests",
}


def unpaid_boundaries(style: str) -> list[str]:
    """Deployment boundaries no driver has agreed to pay for."""
    extra = STYLE_DEPLOYABLES[style][1:]
    return [name for name in extra if name not in PAYING_DRIVER]


assert unpaid_boundaries("monolith_plus_worker") == []
assert unpaid_boundaries("eligibility_service") == ["eligibility"]

Twenty months later a second team took over policy, and the driver that had been missing finally arrived. The extraction took about three weeks rather than a quarter — not because anyone predicted it, but because the import rule had been enforced the whole time, so the seam was still where the boundary map said it was. That is the return on keeping a seam you are not yet using: the option stays exercisable.

Field notes · style-choice smells
You hearThe defectThe move
“We should use microservices.”Style before drivers.Write the drivers first.
“It's cleaner as a service.”Aesthetic without a driver.Price the network rent.
“The database is shared anyway.”Ownership is fiction.Name the writer or split.
“The rules are in a README.”No enforcement.Turn them into fitness functions.
What people get wrong after reading this
“A modular monolith is just a monolith with folders.”
The difference is enforcement. Without a check that fails the build when a module reaches across a seam, the modules are a naming convention and will be gone in two quarters. The fitness function is not a nice extra; it is what makes the word “modular” true.
“Microservices scale better.”
They scale independently. A single process usually has more headroom per unit of complexity at this size; what justifies distribution is scale asymmetry — one component needing far more capacity than the rest — not scale itself.
“Start with services; you can always simplify later.”
Merging services is a data-ownership project with a migration attached. Splitting a well-enforced module is a refactor. The cheapness runs one way, which is why the default runs the other way.
“The style is decided once, at the start.”
Drivers change: a second team, a regulator, a component whose load grows tenfold. The ADR should carry its revisit trigger, and someone should actually check it — a decision with no trigger becomes a tradition.

When this is the wrong move. The modular-monolith default is wrong when a driver is already known and binding: a regulated workload that must run in an isolated environment; a component that needs a different runtime, such as a model server; two teams that already exist with different release cadences and a shared repository as their actual bottleneck; or one component whose load is a hundred times the rest, where scaling the whole process is genuinely wasteful. In those cases the boundary is not an option you are preserving — it is work you would be deferring at a cost. Draw it, price it in the ADR, and say out loud which drivers pay for it.

Skill: derive the style

Choose the architecture style after…

Extracting eligibility into a service changes journey-to-eligibility calls by…

For a small team the default is…

Practice: write the driver-to-style argument

Use your top five measurable drivers, or start with the supplied returns driver table. Compare at least two credible styles against every driver and include operational simplicity as a cost. Choose the smallest adequate style, record one accepted weakness, and name the evidence that would trigger reconsideration. For every distributed boundary complete: “We accept network, versioning, observability, and ownership overhead because independent ____ is required by driver ____.”

In the lab and the capstone: the companion lab's phase-5 checkpoint ends with the structure the earlier phases produced. Write the import rule that would make its boundaries real, run it, and count the violations — that number is your honest starting position. The worked capstone shows the comparison and its record in options and decision records, traced back to the measurable drivers that paid for each boundary.

Reveal: the empty-blank test

If the second blank reads "cleanliness," "modernity," or "maybe later," you have not found a driver — keep the module seam and postpone the distributed system. The seam preserves the option; the service pays rent for nothing.

Teach it back
  1. Explain to a senior engineer, in two sentences, what distribution actually buys and what it converts in exchange.
  2. Take one system you own: name each deployable and the driver that pays for it. If a boundary has no payer, say what it would take to remove it.
  3. Predict the damage: if a six-person team runs eight services for a year, name what shows up in their incident reviews and in their lead time.

Your win

You can derive an architecture style from measurable drivers, justify every deployment boundary with a named cost and driver, and enforce the result with fitness functions.

Read and watch deeper

Ask your agent-teacher to defend the style you did not choose as if it were the obvious answer — then rebut it with your driver table. The rebuttal is the ADR.