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.
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.
| Driver | What it demands | Style response |
|---|---|---|
| One small team, one deployable | Low coordination cost, simple ops. | Modular monolith — modules, one process. |
| Policy changes weekly | Fast, isolated change in one module. | Module seam with enforced dependency rules. |
| Carrier and payments are external | Translate at the edge; replace vendors. | Ports and adapters at the boundary. |
| Compliance audit of decisions | Auditable facts, no accidental writes. | Event or history stream inside the module. |
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:
- Independent scaling of one component under measured load.
- Isolation — a security, regulatory, or availability boundary that one deployable cannot honor.
- Independent release cadence — teams that must deploy on their own beat.
- Technology freedom — a component that genuinely needs another runtime.
- Ownership — a team that must own its slice end to end (lesson 0036).
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 | Modular monolith | Monolith + outbox worker | Eligibility as a service |
|---|---|---|---|
| Availability: carrier down 30 min, 99% of labels within 15 min of recovery | Fails — a synchronous carrier call ties the request to the vendor | Meets it: intent persisted, worker retries | Meets it, but only because it also has a worker |
| Performance: 40 rps, eligibility decision p95 < 800 ms | Meets it with room | Meets it with room | Meets it; one network hop spends ~10–30 ms of the budget for nothing |
| Modifiability: one engineer ships a policy rule in a day | Meets it | Meets it | Worse: a rule that touches the contract needs two deploys, ordered |
| Operability: label failures > 2% for 5 min, disable within 10 min | One process to watch, one flag | Two processes, one flag; queue depth is a new signal | Three processes, two on-call surfaces, six-person team |
| Security: object-level authorization on every read | One place to enforce it | One place to enforce it | Enforced 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.
| You hear | The defect | The 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. |
- “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.
- Explain to a senior engineer, in two sentences, what distribution actually buys and what it converts in exchange.
- 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.
- 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
- Fundamentals of Software Architecture, Richards & Ford — ch. 2–8: architectural thinking, modularity, characteristics and component-based thinking; then ch. 10–19: the styles and how to choose.
- Monolith to Microservices, Sam Newman — ch. 1–3: what a service boundary must buy to be worth its cost, and how to extract one from a monolith when a driver finally arrives.
- Learning Domain-Driven Design, Khononov — ch. 8–10: tactical patterns and the architectural styles that preserve model boundaries.
- Related lessons: 0044, 0025; glossary fitness function, architecture driver.
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.