Drawing Boundaries · Nº 24

Drawing Boundaries

A field guide to system architecture

The monolith-vs-microservices debate argues about deployment count when the real variable is coupling — how far a change here propagates over there. This guide teaches the decision underneath the debate: how to draw seams that minimize cross-talk, enforce them inside one deployable for as long as possible, and recognize the specific pressures that justify paying latency, partial failure, and distributed debugging to make a seam physical.

Module 01 What you're actually deciding

You have been in this meeting. Someone says the monolith is holding the team back; someone else says microservices are a distributed mess; both cite a war story, and the story is true in each case. The argument never resolves because both sides are describing the same disease at different deployment counts. The unshippable monolith and the microservice estate nobody can change are both systems where a change here forces a change over there — and that property, not the number of deployables, is what you are actually deciding when you draw an architecture.

This module installs the frame the rest of the course runs on. It defines coupling and cohesion tightly enough that you can count them on a real repository rather than gesture at them, and it separates two decisions that get fused in almost every architecture conversation: where the module boundaries go, which is a design decision you can make inside one process for free, and where the deployment boundaries go, which is a cost decision layered on top. Once those come apart, the question stops being “monolith or microservices?” and becomes something you can answer with evidence.

The argument you've been having is the wrong one

“Monolith or microservices” is a question about how many things you deploy. It is asked as though the answer predicts whether the system will be pleasant to change in two years. It does not. Consider the two canonical horror stories. The first: a monolith where adding a field to the checkout flow means touching nineteen files across four notional subsystems, and the release is a Thursday-night event with a rollback plan. The second: a twelve-service estate where adding the same field means a coordinated change to four services, three contract versions, and a release order documented in a wiki page nobody trusts. Both teams are miserable, and they are miserable in exactly the same way — a change in one place forced changes in many others.

The deployment count differs. The disease does not. What both systems have is coupling: change propagation across boundaries that were supposed to contain it. In the first system nothing stopped the propagation because there were no internal boundaries; in the second, the boundaries were drawn in the wrong places and then made physical, which converted a code problem into an operations problem and added a network bill.

So the guide's promise is a substitution. Stop asking how many deployables you should have. Ask where the seams in this system actually belong, and — separately — which of those seams you are ready to pay to make physical. Both halves of that question have evidence-based answers, and the rest of this course is how to get them.

The load-bearing idea

Deployment count is not a proxy for architectural quality. Coupling is the variable; the number of deployables is a downstream cost decision that can improve, worsen, or do nothing to a system depending entirely on whether the seams were in the right place first.

Coupling and cohesion, precisely

Both words are used as compliments and insults rather than measurements. Tighten them.

Coupling is the degree to which a change in one part of a system forces a change in another. It is a property of a pair of parts and it is directional in practice: cellar may depend on valuation heavily while valuation is indifferent to cellar. Cohesion is the degree to which the things that change together live together. It is a property of a single part. The two are not opposites; they are the same evidence read from different sides. If two modules change in the same pull request every week, that fact simultaneously says the coupling between them is high and the cohesion of each is low — the thing that actually changes as a unit is the pair, and your boundary has cut through it.

What makes these definitions useful rather than decorative is that they are countable. Take the last three months of your repository's history. For each pull request, list the modules it touched. Any PR touching two modules is one change-ripple crossing the boundary between them. Sum by pair and you have an empirical coupling map that owes nothing to anybody's opinion of the design. A good boundary is one that few ripples cross; a bad one is a line that half your commits step over.

Six modules with their change-co-occurrence edges, overlaid with two candidate seams: a vertical seam crossed by two edges and a horizontal seam crossed by sevenSeam A — 2 crossingsSeam B — 7 crossingscatalogcellarvaluationpurchasinglabelsrecsEach line = one pair of modules that changed together in the same PR, last 90 days9 co-change edges total · Seam A cuts 2 · Seam B cuts 7
Figure 1.1 — Boundary quality is countable. Nine co-change edges drawn from ninety days of pull-request history. The vertical seam (A) separates valuation and recs from the rest and is crossed by two edges; the horizontal seam (B) separates “top” from “bottom” modules and is crossed by seven. Same system, same evidence: seam A is a boundary, seam B is a line drawn through the middle of something that changes as a unit.
From your other life

This is contract drafting. A well-drafted agreement allocates future disputes to the party best positioned to handle them, and its quality shows up years later in how many disagreements have to escalate past the four corners of the document. A well-drawn module boundary allocates future changes the same way, and its quality shows up in how many pull requests have to cross it. In both crafts the drafting is cheap and the misallocation is expensive, and in both, the parties keep performing the deal you actually wrote rather than the one you meant.

Module boundary versus deployment boundary

Here is the separation that makes the whole subject tractable. A module boundary is a design decision: which code may call which, what the public interface is, who owns the data, what stays private. You can make it inside a single process, in an afternoon, for free — no new infrastructure, no latency, no operational surface. A deployment boundary is a cost decision: the same seam made physical, so that the two sides are built, released, scaled, and fail separately. You pay for it in a network hop, a versioned contract, an operational footprint, and a debugger that stops at the edge.

Two consequences follow immediately, and they carry the rest of the course. First, the second decision never substitutes for the first. Distributing badly-separated modules does not force them to become well-separated; it converts an in-process tangle into a network tangle with a monthly bill and an on-call rotation. Second, the two decisions can be sequenced. Get the module boundaries right first, inside one deployable, where a mistake costs a refactor rather than a migration. Then, if and only if a named pressure justifies it, make one seam physical.

Quadrant matrix of internal module boundary strength against deployment boundary count, placing big ball of mud, modular monolith, distributed monolith, and well-factored microservicesDistributed monolithevery cost in the ledger,none of the purchases“five services, one database,Thursday deploy train”Well-factored microservicespurchases collected,invoice knowingly paidone owner per store,independent release per serviceBig ball of mudany code calls any code,any query joins any tablecheap to run,expensive to changeModular monolithstrong seams, one deployable —this guide's defaultone call stack, one transaction,enforced facades1. right: strengthen the seams2. up,one seamat a timefatal: up before rightInternal module boundaries: weak → strongmanydeployablesonedeployableDeployment boundaries
Figure 1.2 — The master matrix. The horizontal axis is a design decision (how strong are the internal module boundaries); the vertical axis is a cost decision (how many separately deployed units). The four inhabitants are the four combinations. The safe path moves right before it moves up: strengthen the seams inside one deployable, then make a seam physical when a pressure justifies it. Moving up before right lands you in the top-left square, where you pay the full distributed-systems invoice and collect nothing, and every later module in this guide refers back to this figure.

Reading the quadrants

Walk the four squares with a recognizable inhabitant in each.

Weak boundaries, one deployable — the big ball of mud. Any code can call any code; any query can join any table. It is cheap to operate and expensive to change, and its defining property is that nobody can predict the blast radius of a two-line change. This is the square most systems drift into by default, because drift is free and boundaries are not.

Strong boundaries, one deployable — the modular monolith. The same single process, but modules have narrow public facades, private internals, and their own data. Most of the design benefit of services, none of the network tax. This guide's default, argued in module 2.

Strong boundaries, many deployables — well-factored microservices. Each service owns a capability and its data, deploys on its own cadence, and is called through a versioned contract. The purchases in module 3 are genuinely collected here, and the invoice is genuinely paid.

Weak boundaries, many deployables — the distributed monolith. Services that must change and release together, usually because they share a database or a release train. This is the worst square on the board, and it is worth being precise about why: it pays every line on the invoice — latency, partial failure, contract maintenance, multiple pipelines, distributed debugging — and collects none of the purchases, because nothing can actually be deployed or scaled independently. A big ball of mud at least gets a call stack and a transaction for its trouble. Module 5 performs the autopsy.

Anti-pattern: microservices as a default or a résumé line

Symptom: the architecture diagram has more boxes than the team has engineers, and when you ask which pressure motivated any particular split, the answer is a shape (“that's how you build things now”) rather than a measurement. Frequently accompanied by a service whose entire job is one CRUD table.

Corrective: name the pressure — divergent scaling, team contention, release-cadence conflict, fault isolation, technology mismatch — and produce the evidence for it, or stay in one deployable. Module 8 turns this into a runnable gate; for now, the discipline is simply that “split” requires a named reason and “don't split yet” does not.

One warning about using the matrix. A system's diagram tells you where its authors believe it sits. Its pull-request history, its schema access list, and its deploy log tell you where it actually sits. When the two disagree, the evidence wins — that disagreement is itself the most useful architectural finding you can make in your first month on a codebase.

Module 02 The monolith, honestly

The word “monolith” has become a diagnosis rather than a description. That is a problem for clear thinking, because a single deployable gives you four things that distributed systems spend enormous effort partially recovering, and you cannot price a split honestly if you refuse to name what the split is spending. This module states the assets plainly, then explains how monoliths actually die — which is not from being monoliths — and then builds the architecture this guide recommends by default.

The through-line: the deployable was never the problem. The boundaries were. A single process with enforced internal seams gets you most of what people go to microservices for — independent reasoning, clear ownership, testable interfaces, the ability to hand a module to a new engineer with a straight face — before you pay any part of module 3's invoice. The catch, and it is a real one, is that a boundary nobody enforces is not a boundary. The last section is about enforcement, because that is where this architecture succeeds or quietly becomes the top-left quadrant.

What a single deployable does well

Four assets, stated without irony, because module 3 will charge you for every one of them.

One call stack. When a request misbehaves, the stack trace contains the whole story, in order, on one clock. You can set a breakpoint at the top and step to the bottom. Every observability product sold to distributed systems is an attempt to reconstruct approximately this, and the reconstruction is never as good as the original.

One transaction. Several modules can write within a single BEGIN…COMMIT, and either all of it happened or none of it did. This is atomicity across module boundaries, for free, with no compensation logic, no partial states, and no user-visible intermediate moment. It is the most expensive thing on this list to give up, which is why the next section is devoted to it.

One deploy. There is no version matrix and no contract skew: the code that calls and the code that answers ship together, always. Refactoring across a module boundary is a pull request, not a two-phase rollout with a compatibility window.

One grep. The whole system answers a text search. “Who calls this?” is a question with a complete answer, computable in seconds, and it stays complete as the system grows.

Note

Monolith does not mean legacy, and it does not mean small. It means one deployable. Plenty of systems serving serious traffic are single deployables on purpose, and the burden of proof belongs on the split — not because splitting is wrong, but because splitting spends these four assets and you should know what you bought with them.

The single transaction, appreciated

Take the wine-cellar product's record a purchase operation. A user buys a 2016 Barolo at auction and records it. Three modules write: purchasing inserts the acquisition and decrements the user's budget, cellar inserts the bottle at a location, and valuation writes the cost basis that every future appraisal is measured against. In a monolith this is one function calling two facades inside one database transaction.

Now consider the failure. Suppose valuation raises because the market-data provider has no comparable and the cost basis cannot be established. The exception propagates, the transaction rolls back, and the database returns to the state it was in before the request. There is no orphaned bottle in the cellar, no phantom budget decrement, and no “acquisition recorded but unvalued” state for someone to discover in a support ticket eight weeks later. The user sees one clean error. You wrote no code to make any of that true.

Sequence diagram of record-a-purchase across purchasing, cellar, and valuation inside one database transaction, with a mid-flow failure rolling everything backpurchasingcellarvaluationBEGINCOMMIT (never reached here)cellar.add_bottle(barolo_2016)INSERT bottle · okvaluation.record_cost_basis(...)raises NoComparableSaleexception propagates, unhandledROLLBACK — the bottle insert is undone tooNo compensation code. No partial state. No intermediate moment any user could observe.Cutting a service boundary between any two of these lanes replaces this bracket with a saga (module 7).
Figure 2.1 — Atomicity across modules, for free. Three modules write inside one BEGIN…COMMIT. When valuation fails, the bottle that cellar already inserted disappears with it — not because anyone wrote cleanup logic, but because the database was told these writes were one unit. This is the single most expensive asset you will ever give up at a service boundary; module 7 shows exactly what replaces it.
Cross-reference — Guide Nº 06

What is doing the work here is ACID's atomicity and isolation, and Guide Nº 06 covers the mechanics: why the intermediate state is invisible to concurrent readers, what isolation level you are actually running under, and why “rolled back” is a promise about the log, not about time travel. Read that guide's transaction chapter alongside this section if the guarantee still feels like magic — the point of this module is that it is a purchased guarantee, and module 7 is where you find out the price.

How monoliths actually die

The big ball of mud is real. What is false is the causal story usually attached to it — that single deployables inevitably become mud with age. They become mud when internal boundaries are absent or unenforced, and the mechanism is worth stating precisely because it explains why distribution does not cure it.

Every shortcut that erodes a boundary is locally rational. It is Thursday, the fix is needed, and calling valuation's internal pricing helper directly saves an hour versus extending its facade. The engineer who does it is not careless; they are correct about this ticket. The cost is diffuse, deferred, and paid by someone else — which is the exact shape of a decision that gets made every time. Meanwhile the reverse move, removing a cross-boundary dependency, is expensive, benefits nobody visibly, and appears on no roadmap. So the entropy is one-directional. Boundaries do not erode because people are bad; they erode because the local incentive always points the same way and nothing is pushing back.

And the death is gradual. Nobody notices the eleventh direct import. What people notice, eventually, is that estimates have doubled, that changes cause unrelated breakage, and that new engineers take two quarters to become productive. By the time it is visible in velocity, the boundary evidence — module 1's edge count — has been rising for a year. That is the argument for measuring it: co-change is a leading indicator, and velocity is a lagging one.

Anti-pattern: “the monolith made us do it”

Symptom: a rewrite or migration proposal whose diagnosis section blames the deployment model for problems that are all internal-discipline problems — cross-module joins, no ownership, direct imports of other modules' internals, a test suite that must run whole. The tell is that the proposal contains no enforcement mechanism, only a new topology.

Corrective: attribute the rot to the missing enforcement, and notice that it will follow you across the network. A team that could not maintain an import rule in CI will not maintain a service contract across two repositories — the same shortcut is still locally rational, it just now takes the form of a shared table or a synchronous relay. Fix the boundaries where they are cheap to fix, and then decide whether any of them should be physical.

The modular monolith

The modular monolith is the bottom-right quadrant: strong internal seams, one deployable. Four properties define it.

A narrow public facade per module. valuation exposes current_value(bottle_id), record_cost_basis(...), and a handful of others. Everything else — the ORM models, the comparable-sales engine, the provider client — is internal and unimportable from outside.

Private internals, enforced. The facade is only a facade if the alternative paths are closed. That is the next section's entire subject.

Data ownership. Each module owns a schema namespace. cellar writes cellar.* and never joins to valuation.*; it asks valuation. This one rule does more work than the other three combined, because — as module 5 argues at length — the database is where coupling hides best.

Explicit dependencies. The allowed module-to-module edges are written down, and the graph is acyclic. When someone needs a new edge, that is a design conversation with an artifact, not an import.

What you get is most of the design benefit people pursue through microservices: you can reason about a module without holding the system in your head, hand it to a team with real ownership, test it against its interface, and — this is the part that matters for later — extract it if a pressure ever justifies it, because the facade is already the seam. What you have not paid: no network hop, no contract versioning, no second pipeline, no partial failure, no distributed trace to reconstruct one request. That combination is why this is the guide's default rather than a compromise.

The same wine-cellar system drawn three ways to one template: tangled monolith, modular monolith, and microservices, with the deployment boundary highlighted in each(a) monolith, no internal walls(b) modular monolith(c) microservicesany code calls any codefacades only · schema per modulesame seams, now physicalaccountscatalogcellarvaluationpurchasinglabelsrecsone Postgres · no schema rulesaccountscatalogcellarvaluationpurchasinglabelsrecsone Postgres · schema per moduleaccountscatalogcellarvaluationpurchasinglabelsrecs7 stores,7 pipelines,7 contractsThick outline = deployment boundary. Panels (b) and (c) have identical seams;the only difference is which of them are physical — and therefore which ones cost money.(a) has one deployment boundary and no module boundaries: bottom-left of Figure 1.2.(b) bottom-right. (c) top-right — reached from (b), never from (a).
Figure 2.2 — One system, three renderings. The same seven wine-cellar modules drawn to one template. In (a) the module boxes are dashed because they are only naming conventions — call edges cross everywhere and one unpartitioned database sits underneath. In (b) the boxes are real: facade-only edges, a schema per module, one deployment boundary around the whole. In (c) every module keeps exactly the seams of (b) and each gets its own deployment boundary, its own store, and a contract on every remaining edge. The seams in (b) and (c) are identical — which is the whole argument for reaching (c) from (b) rather than from (a).

Enforcing seams without a network

Here is the honest weakness of this architecture, stated before an advocate of services says it for you: a service boundary is enforced by physics. You cannot import another service's internals over HTTP; the compiler will not let you and neither will the network. A module boundary has no such help. It is enforced by whatever you build, and if you build nothing, it is a comment.

So the modular monolith's viability rests entirely on substituting a mechanism for that physics. The menu is short and the choices compose.

MechanismWhat it catchesWhat it costsWhere it leaks
Language visibility (package structure, explicit __all__ / __init__ exports, internal subpackages)Casual reaching-in; makes the facade the obvious pathNear zero; a naming and layout conventionPython cannot stop a determined import; conventions decay silently
Import linting / architecture tests in CI (e.g. an import-linter contract per module)Every forbidden module-to-module import, on every pull request, by nameA config file and a build step; occasional argument about exemptionsExemptions granted “temporarily” and never removed — the single most common failure
Schema ownership: one namespace per module, no cross-namespace joinsThe coupling that hides in SQL, which import rules never seeSome queries become two calls or a facade methodReporting and analytics paths that get a read-everything credential (module 5)
Facade with private types (internal ORM models never returned across a seam)Structural coupling to another module's data shapeMapping code at the boundary; some duplication of dataclassesA “shared types” package that quietly becomes a second shared database
Code ownership rules requiring a facade owner's review to change itSilent widening of the public interfaceReview latency on interface changes — which is the pointNothing, if ownership is real (module 4 is about when it is not)

The pattern across the table: the cheap mechanisms catch honest mistakes and the CI mechanisms catch the deliberate ones. Only the CI mechanisms actually hold, because only they fail the build on a Thursday afternoon when the shortcut is locally rational. That is the whole trick. Enforcement in CI is the monolith's substitute for the network's brutal but effective refusal — less absolute, considerably cheaper, and adjustable by a pull request rather than a migration.

The load-bearing idea

A module boundary that does not fail CI is a suggestion, and suggestions lose to deadlines. If you adopt one thing from this module, adopt an import-linter contract and a no-cross-schema-join test, and treat exemptions as debt with an owner and a date — because the exemption list is where modular monoliths turn back into mud.

Module 03 What a boundary buys — and costs

Module 1 separated the module boundary from the deployment boundary and called the second one a cost decision. This module is that cost decision, priced in both directions. It is the longest module in the course because it is the one that makes every later decision adjudicable: module 6's extraction, module 7's wiring, and module 8's framework all run the ledger established here.

Two disciplines make this ledger honest. First, each purchase comes with a pay-out condition — the circumstance under which you actually receive what you bought. Independent deployment that requires a coordinated release is not independent deployment; fault isolation that the caller does not honor is a bulkhead with a propped-open door. Second, the invoice is stated in numbers where numbers exist. A remote call is not “a bit slower” than a function call; it is four to six orders of magnitude slower, and the arithmetic changes what designs are viable in ways that intuition does not.

What you're buying

Four purchases. Each is real, and each has a condition attached that is skipped in most proposals.

Independent deployment. You can ship one side of the seam without shipping the other. Pays out only if the contract between them is stable enough that most changes are one-sided. If every feature requires a coordinated change to both, you have bought a coordination ritual and paid a network hop for it — that is the distributed monolith, arriving one plausible decision at a time.

Independent scaling. You can run twelve of one and two of the other. Pays out only if the load profiles genuinely diverge. Two components that receive the same requests at the same rate scale together whether or not they are separate processes; splitting them buys nothing and costs a hop.

Team autonomy. A team can change its service without negotiating with another team. Pays out only if ownership and on-call move with the service. A service owned by a committee, or built by one team and paged by another, produces more coordination than the module it replaced — module 4 is entirely about this condition.

Fault isolation. A failure on one side does not take down the other. Pays out only if the caller handles the fault. This is the condition most often assumed rather than met, and it is worth being blunt: extracting a service does not isolate anything by itself. If the caller awaits it synchronously with no timeout and no fallback, you have moved the failure across a network and added a new way for it to happen.

From your other life — security

Fault isolation is blast-radius containment, and the reasoning is identical to network segmentation. Putting a subnet boundary between two tiers buys nothing if the security group allows everything through it; the control is the rule, not the topology. Same here: the service boundary is the segment, the timeout and fallback are the rule, and a bulkhead nobody honors is a diagram artifact. When you review an extraction proposal, ask the question you would ask of a segmentation design — what specifically is denied at this boundary, and what does the caller do when the deny fires?

Memory footprint comparison: eight replicas of the whole wine-cellar monolith versus eight slim application replicas plus two recommendation service replicasScaling the monolith for catalog trafficScaling with recs extracted8 replicas × 1.8 GB (each carries the 1.2 GB model)8 × 0.6 GB app + 2 × 1.5 GB recsshaded band = idle model memory, replicated 8×total ≈ 14.4 GBrecs serves 4 req/s; catalog serves 300 req/s.The load profiles diverge by ~75×.8 slim app replicas · 2 recs replicas (accent)total ≈ 7.8 GBEach side now scales on its own signal:request rate for the app, queue depth for recs.The purchase pays out here because the load profiles genuinely diverge. If recs took 300 req/s too, both pictures would be the same size.
Figure 3.1 — Independent scaling, when it actually pays. The wine-cellar monolith must replicate the whole image to serve catalog page views, so eight replicas each carry the 1.2 GB recommendation model that answers four requests per second. Extracted, the application scales on request rate and recs scales on its own, roughly halving the footprint. The purchase is real because the load profiles diverge by about 75×; that divergence, measured, is the evidence a split proposal has to contain.

The hop, dissected

An in-process call is a call instruction: push a frame, jump, return. Tens of nanoseconds, and the only way it fails is by raising, which the caller sees immediately and completely. Making that call remote replaces it with a pipeline of eight or so distinct stages, each with its own latency and its own way to fail.

The order-of-magnitude figures below are binding for this course and consistent with Guide Nº 14's treatment of the layers underneath.

OperationOrder of magnitudeNotes
In-process function call~10–100 nsNo serialization, no copy of anything large
JSON serialize + deserialize, KB-scale payload~0.1–1 msPaid twice per round trip — request and response
Same-AZ round trip~0.5–1 msExcludes remote processing time; TLS handshake adds more on a cold connection
Cross-region round trip~30–100 msSpeed of light plus routing; no engineering removes it

Read the first and third rows together. A same-AZ remote call is roughly ten thousand times the cost of the function call it replaced, before the remote side does any work. That ratio is why call frequency across a candidate seam is boundary-placement evidence and not a detail: a call made once per request disappears into the noise, and the same call made two hundred times per request is a redesign.

The same call drawn twice: as one in-process invocation, and expanded into the serialize, connect, transmit, queue, execute, respond, and deserialize stages of a remote call with latencies and failure modes annotatedIn process — module 2's worldcellarrecs.recommend(user, 5)~50 ns · outcomes: returns or raisesrecsAcross a service boundary — the same call, expandedserialize~0.2 msconnect/TLS0 or ~2 mstransmit~0.3 ms AZqueue0 → ∞deserialize~0.2 msexecutethe actual workrespond~0.5 msdeserialize~0.2 msNew failure modes, by stage:serialize — payload unrepresentable, schema driftconnect — refused, DNS failure, TLS expiry, pool exhaustedtransmit — packet loss, retransmit, path changequeue — slow success: arrives after the caller gave upexecute — 5xx, partial write, remote timeoutrespond — the work happened, the answer is lostsame-AZ round trip ≈ 0.7–1.5 ms overhead · ~10,000× the function call it replacedCross-region substitutes ~30–100 ms for the transmit and respond stages; nothing else changes.“It's just a function call over HTTP” is false at both ends: the latency and the failure set are different in kind.
Figure 3.2 — Anatomy of the network hop. One call instruction becomes eight stages, each contributing latency and each contributing a failure mode the in-process version could not have. The overhead alone is roughly 0.7–1.5 ms same-AZ — about ten thousand times the call it replaced — before the remote side does any work. Cross-region swaps 30–100 ms into the transmit and respond stages. The stage that matters most for module 7 is respond: the work has already happened and the answer can still be lost.
Cross-reference — Guide Nº 14

Every stage in the bottom lane is a chapter of Guide Nº 14: connection setup and TLS, the request/response cycle, keep-alive and pooling, and why the first call on a cold connection costs several times the steady-state one. If the numbers in the table feel arbitrary, that guide derives them. This module takes them as given and asks a different question — what they do to your design.

Partial failure and the ambiguous timeout

Latency is the invoice line people expect. The one that reshapes code is partial failure, and its sharpest form is worth isolating.

An in-process call has exactly two outcomes: it returns, or it raises. Both are observed by the caller, immediately and unambiguously. There is no third case, because the callee's execution and the caller's knowledge of it are the same event.

A remote call has four. It can succeed. It can fail explicitly — connection refused, a 4xx, a 5xx — which the caller sees and can act on. It can succeed slowly, arriving after the caller has already given up and moved on, which means the work happened and something downstream may now be inconsistent with what the user was told. And it can time out with the work's fate unknown: the request may never have arrived, or it may have arrived, executed, mutated state, and had its response lost on the way back. From the caller's chair those two are indistinguishable, and no amount of client-side cleverness distinguishes them, because the distinguishing fact exists only on the other side of the boundary.

The load-bearing idea

The ambiguous timeout is physics, not a bug. Once a seam is physical, every caller must answer three questions at the time it writes the call: how long do I wait, what do I do when I stop waiting, and is retrying safe. A remote call written without all three answers is not a call, it is an incident with a delay fuse.

State machine comparison: an in-process call with two terminal outcomes beside a remote call with four, including the ambiguous timeout drawn as a dashed branchIn-process call — 2 outcomescallreturnedraisedBoth are observed. The caller alwaysknows whether the work happened.Remote call — 4 outcomescallsuccessexplicit failureslow success(caller already gave up)ambiguous timeoutdid the work happen?The dashed branch is unanswerable from the caller's side. Safety comesfrom the callee recognizing resends — an idempotency key — not from guessing.
Figure 3.3 — Two outcomes become four. An in-process call returns or raises, and the caller observes which. A remote call adds slow success — the answer arrives after the caller moved on — and the ambiguous timeout, where the work may or may not have happened and the caller cannot tell. Every design across a physical seam must decide what to do on the dashed branch, and the only real answer is to make retries safe on the receiving side.
Cross-reference — Guides Nº 07 and Nº 17

The mechanism that makes the dashed branch survivable is the idempotency key, covered fully in Guide Nº 07: the caller labels the operation, the callee recognizes a resend and replays its stored response instead of re-executing. Guide Nº 17 covers the delivery semantics behind it — at-least-once as the honest default and exactly-once as an engineered illusion. This guide will not reteach either; it will insist you decide, at every physical seam you create, which of your calls are safe to retry and what makes them so.

Debugging across the boundary

The asset module 2 called “one call stack” is the one you miss at 3 a.m.

Here is the actual experience. A user reports that their cellar page is slow and sometimes shows no recommendations. In the monolith, you reproduce it, set a breakpoint in cellar.page(), and step down into recs.recommend(); the stack trace names every frame; the timing is one profile. After extraction, you have two services' logs, written by two processes on two hosts with clocks that agree to within some tolerance you have never measured, and “step into” stops dead at the serializer. The question “what happened to this request?” is no longer answerable by reading; it is answerable only by reconstruction, and only if you built the reconstruction in advance.

The partial replacement is a correlation ID minted at the edge and propagated on every call, plus distributed tracing so one user action can be reassembled from spans across services. It works. It is also a real line on the invoice, and proposals routinely omit it: the instrumentation in both services, the propagation discipline on every new call path, the trace backend and its retention bill, the sampling policy that will hide exactly the rare request you need, and the ongoing cost of a team learning to debug by query rather than by breakpoint.

Walk the concrete page. At 03:12 the alarm is elevated p99 on GET /cellar. Correlation ID in hand, the trace shows 40 ms in cellar, then a 2,000 ms span waiting on recs, which itself shows 30 ms of work and 1,970 ms of queue wait — recs is not slow, it is under-provisioned after a deploy halved its replica count. Without the trace, the visible symptom is “the cellar page is slow,” the on-call engineer for cellar owns the alarm, and the actual fault is in a service they do not operate. That last sentence is the real cost: the boundary moved the symptom away from the cause, and only instrumentation moves it back.

The ledger, totaled

Run the whole thing on the running example. The wine-cellar team proposes extracting recs. Here is the ledger, in the format module 6 and module 8 both reuse.

PurchasePays out?Evidence
Independent scalingYesCatalog paths serve ~300 req/s; recs serves ~4 req/s and holds a 1.2 GB model. Eight replicas carry it for nothing (Figure 3.1). Divergence ≈ 75×.
Team autonomyYesThe ML team has owned recs's code for two quarters and wants to ship model updates weekly; product engineering releases fortnightly, and the model updates currently wait.
Independent deploymentYes, conditionallyrecs appeared in nine PRs last month and co-changed with no other module (module 1's count). The contract is one method. Condition holds today; re-check if the interface starts growing.
Fault isolationOnly if builtNothing about the split isolates anything. Pays out only with the timeout and fallback below, which are therefore part of the proposal, not a follow-up ticket.

The invoice, itemized. Latency: one same-AZ call on the cellar page path, +~2 ms at p95 including serialization, measured against a 220 ms page budget — acceptable, and notably it is one call per page render, not per bottle. Failure: a new dependency that can be down, slow, or ambiguous; the fallback is designed now — on timeout at 150 ms, render “recently added to your cellar” instead of recommendations, with a cache of the last successful recommendation set per user as the first fallback tier. Operations: a second pipeline, a second alarm set, and the ML team joins the on-call rotation for their service. Observability: correlation-ID propagation and tracing on the cellar path, roughly a week of work. Contract: one versioned interface, owned by the ML team, with a consumer-driven contract test in both pipelines.

Judgment: worth it — and it would not have been six months ago, when one team owned both sides and the model was 80 MB. Nothing about the code changed to make this true; the pressure did.

Anti-pattern: “the network is reliable” thinking

Symptom: a newly created remote call with no timeout (or a default of 30 seconds, which is the same thing), no fallback path, and a retry wrapper added without any idempotency guarantee on the receiving end. In review it reads as clean code, because everything it is missing is invisible.

Corrective: every call across a physical seam gets three things at the moment it is written — an explicit timeout chosen from the caller's budget, a designed fallback for when the timeout fires, and an explicit answer to “is this safe to retry, and what makes it safe.” Reviewers should treat their absence the way they would treat an unhandled exception, because that is exactly what it is.

Module 04 The org chart is an architecture diagram

This is the shortest module in the course and the one that explains the largest number of failed migrations. Module 3 priced the technical invoice of a service boundary. This module covers the force that decides whether the boundary holds at all — and it is not a technical force.

Conway's law is usually quoted as a wry observation, which is a way of not taking it seriously. Take it seriously: it has a mechanism, it operates on a timescale of quarters, it is not optional, and once you can see it you can use it. The practical payoff is a diagnostic that costs nothing — when an architecture change is not taking, check whether ownership moved — and a maneuver that works: change the org, and the system follows.

The law and its mechanism

Conway's law: organizations design systems that mirror their own communication structure. Stated that way it sounds like a joke about meetings. The mechanism makes it a force.

Coordination has a cost, and the cost is not uniform. Two engineers at adjacent desks on the same team coordinate continuously and nearly for free: they change an interface between their code in a conversation, or in one pull request, without ceremony. Two engineers on different teams in different time zones with different roadmaps coordinate expensively: changing an interface between their code requires a proposal, a scheduling negotiation, a compatibility window, and someone's quarterly priorities to move.

Now watch what people do under that gradient. Where coordination is cheap, they let the code stay entangled, because untangling has no local payoff. Where coordination is expensive, they do the thing that makes coordination unnecessary: they agree on a stable interface and stop talking about the internals. That agreement is a boundary. So interfaces form precisely where communication is expensive — not by intention, but as the accumulated result of many rational people avoiding meetings.

Two consequences follow. The first is that the law operates whether or not you acknowledge it: a seam drawn on a whiteboard inside a single team's cheap-coordination zone will erode, because nothing makes crossing it costly. The second is that it operates on a timescale of quarters, which is why it is so easy to miss — no single commit is the moment the architecture changed.

From your other life — Amazon

Two-pizza teams and “you build it, you run it” are Conway's law institutionalized on purpose. The service granularity was not derived from a technical optimum; it was derived from a team size, and the architecture was allowed to follow. Notice the second half is the load-bearing one: putting the pager in the same team as the code is what makes the ownership real, which is what makes the boundary hold. A team that owns a service it is not paged for will negotiate its interface like an outsider and treat its internals like a shared resource — you have seen which of those two conditions predicted whether a service stayed clean.

Reading a system as an org document

The law runs in both directions, which makes it a diagnostic. Given an architecture, you can infer the organization; given an organization, you can predict the architecture it will drift toward.

The wine-cellar company has three teams. Product engineering (six people) owns the customer-facing experience: accounts, catalog, cellar, purchasing. The ML team (three people) owns labels and recs. The data team (two people) owns the market-data ingest pipeline and, nominally, valuation. Read the eventual service boundaries off that paragraph and you get the split this course actually performs: recs extracted first (module 6), valuation second (module 7). Nobody chose that order for architectural elegance. It is where the coordination is expensive.

The more interesting reading is the mismatch. valuation is co-owned: the data team owns the ingest and pricing model, product engineering owns the appraisal endpoints the cellar page calls. Neither team can change it alone, and the symptoms are legible without looking at any code — appraisal changes need two teams' sprint capacity in the same sprint, the module has the highest merge-conflict rate in the repository, and its interface has three overlapping ways to ask the same question because each team added the one it needed. Those are not personality problems. They are what co-ownership produces, every time.

An org chart of three teams above a module diagram, with correspondence lines showing each team's ownership and one co-owned module highlighted as a mismatchThe organizationProduct engineering6 engineers · ships fortnightlyML team3 engineers · ships weeklyData team2 engineers · ships on ingest cadenceweekly syncmonthly syncThe systemaccountscatalogcellarpurchasinglabelsrecsvaluationco-owned: two teams, one modulesymptoms: highest merge-conflict rate in the repo;appraisal changes need both teams in the same sprint;three overlapping ways to ask the same questionWhere ownership is singular, the interface isnarrow and stable — the team had a reasonto stop coordinating.Read the top half and you can predict which seams will hold. Read the bottom half and you can name the org problem.
Figure 4.1 — The mirror. Three teams above, seven modules below, with ownership drawn as correspondence lines. Where a module has exactly one owning team the interface is narrow and stable, because the team's boundary with its neighbours is where coordination is expensive. The one co-owned module, valuation, is the system's chronic-friction site — and every symptom it produces is predictable from the ownership line alone, without reading a line of its code.

The inverse maneuver

If the force is real, stop being subject to it and start steering with it. The inverse Conway maneuver: decide the target architecture, then reshape team ownership to match it, and let the law pull the code toward the seams you want.

The sequence matters. Move ownership first, and the interfaces harden along the new lines on their own — because the newly-separated teams now find coordination expensive and start doing what people do about that, which is agreeing on a contract and leaving each other alone. The extraction, when you get to it, is mostly mechanical, because the seam is already load-bearing. Move code first against unchanged ownership and you get the opposite: the boundary is a formality nobody has an incentive to respect, someone adds a shared table to avoid an awkward conversation, and within two quarters the “services” are welded together — module 5's failure mode, produced by an org-chart cause.

The constraint that sizes the result is team cognitive load. A team can own only as much system as it can hold responsibly — understand, change confidently, and be paged for. This is a real limit, and it cuts both ways: a service larger than its team's capacity rots from the inside because nobody knows all of it, and a team owning nine services spends its time on operational surface rather than on the domain. It is also the honest answer to “how big should a service be?” — big enough to be a capability, small enough that one team can hold it.

Two-lane timeline contrasting the inverse Conway maneuver, where ownership moves first and the boundary hardens, with the default sequence, where code moves first against unchanged ownership and the boundary erodesInverse maneuver — ownership firsttarget seams drawnweek 0ownership reassignedon-call moves toointerfaces harden1–2 quarters, on its ownextraction is cheapthe seam already holdsDefault sequence — code first, ownership unchangedservices extractedsame teams own both sidesboundary is a formalitycrossing it costs nobodyshared table appearsto skip a conversationdistributed monolithmodule 5's autopsySame target architecture, same engineers, same quarter of effort. The only variable is whether ownership moved before the code did.Diagnostic: if an architecture change is not taking, ask what changed on the org chart. Usually the answer is nothing.
Figure 4.2 — Two sequences, one destination attempted. Above: ownership moves first, the newly separated teams find coordination expensive and harden an interface on their own, and the eventual extraction is mechanical because the seam is already real. Below: the code moves first against unchanged ownership, nothing makes the boundary costly to cross, and within a couple of quarters a shared table appears to avoid an awkward conversation. Same target, same people; the difference is sequencing.
Anti-pattern: ignoring Conway's law

Symptom: an architecture that requires two or more teams to ship every feature together, and a retrospective culture that attributes this to the teams — poor communication, unclear priorities, a need for better planning. Rituals accumulate: a weekly sync, a shared roadmap document, an integration manager.

Corrective: stop adding coordination and change what needs coordinating. Either move the seam so that a feature lands inside one team, or move ownership so that one team owns both sides. Adding process to a Conway mismatch is paying rent on a problem you could buy out of; the force always wins on a timescale of quarters, and the only choice is whether it wins in a direction you chose.

Module 05 The cardinal sin

Every other form of coupling in this course is at least visible. A synchronous call appears in the code and on a trace. A shared library appears in a manifest. A coupled release appears in a runbook. Data coupling appears nowhere: two services can share a schema while their architecture diagram shows two clean boxes with a tidy arrow between them, and the diagram is not lying about anything it draws — it simply does not draw the layer where the weld is.

That invisibility is why this is the cardinal sin rather than merely a bad idea. It survives design review, it survives the migration to services, and it is discovered only when someone tries to do the thing services were bought for: deploy alone. This module explains why a shared table couples harder than any API could, states the rule that fixes it, is honest about what the rule costs, and then walks a full autopsy of a system that did all of it wrong.

Why the database is where coupling hides

Compare two ways for service B to get data that service A owns.

Through A's API. The contract is explicit: a named endpoint, a documented response shape, a version. A has a list of consumers and a way to notify them. A can change its internal storage — rename a column, denormalize, switch a data type, add a cache — with no coordination at all, because none of that is in the contract. When A does need to change the contract, there is a defined procedure: additive change now, deprecation window, removal later.

Through A's tables. The contract is the schema. Nobody wrote it down as a contract, nobody versioned it, and nobody knows who the counterparties are — the only way to find out is to grep every repository that has the credentials, and the credentials are usually shared. A cannot rename a column, change a type, add a NOT NULL, or split a table without potentially breaking readers it cannot enumerate. In practice, this means A stops migrating its schema at all, or it migrates by convening everyone, which is the coordination the split was supposed to eliminate.

The asymmetry is the point. An API contract exposes a deliberately chosen, narrow surface and hides everything else. A schema exposes the entire internal data model, including the parts that exist for A's convenience and were never meant to mean anything to anyone else. Every column is now load-bearing. Every index choice is now a shared performance budget. Every long transaction A runs is now a lock other services wait on.

So: two services sharing a schema cannot migrate independently, cannot deploy independently in any meaningful sense, and cannot fail independently — a lock or a runaway query in one is an outage in the other. That is not two services. It is one system with a network in the middle of it.

From your other life — security and GRC

Read the shared database as an access-control finding and it becomes obvious. Five services with credentials to one schema is five principals with read and write on every asset — no least privilege, no separation of duties, and no way to attribute a corrupting write to a service after the fact. You would not approve that in a control review, and the architectural argument is the same argument: unbounded authority over another component's state is coupling, whether the harm arrives as a breach or as a Tuesday-morning migration. Guide Nº 06 covers the schema-as-contract idea from the data side; this is the same finding, filed under a different regime.

Database-per-service

The rule is one sentence: every data store has exactly one owning service, and every other service reaches it only through that owner's contract.

Note what it does not say. It does not say every service needs its own database server — separate schemas in one Postgres instance with per-service roles and no cross-schema grants is a perfectly respectable implementation, and it is what the wine-cellar modular monolith already does. The rule is about ownership and access, not about instance count. Nor does it say data cannot be duplicated; it says duplicates are derived from the owner through a contract, not read out of the owner's tables directly.

What the rule buys is the ability to change. The owner can migrate its schema on a Tuesday afternoon without a meeting, because nobody outside depends on the schema — they depend on the contract, and the contract survived the migration. Coupling has not been abolished; cellar still needs valuations. It has been made visible (there is an interface, and a list of who calls it), versioned (changes have a procedure), and negotiable (a consumer can object to a deprecation, in a forum, before it ships). That transformation — invisible and total, into visible and governed — is the entire cure.

Two integration shapes satisfy the rule, and module 7 chooses between them: an API when the caller needs an answer now, and events when the owner is simply announcing that something happened. The choice matters for latency and coupling in ways module 7 develops; for this module's purposes, either one is a contract and the shared table is not.

Two panels: two services reaching into one shared schema with four hidden coupling edges drawn in, beside the database-per-service version with one explicit versioned contract“Two independent services”cellarvaluationone schemaHidden coupling edges (dashed):1. the schema itself — an unversioned two-party contract2. migration lock — neither can alter a table alone3. shared performance budget — one slow query, two outages4. lockstep deploy — schema change forces both to shipDatabase per servicecellarvaluationGET /valuations?bottle_ids=…v2 · deprecation policycellar.* (owner)valuation.* (owner)One edge remains — and it is:visible (it is in the code and on the trace)versioned (v2, with a deprecation window)negotiable (consumers are known and can object)Either side migrates its own schema without a meeting.
Figure 5.1 — The weld, and the contract that replaces it. On the left, two services drawn as independent while four coupling edges run between them through the schema: the unversioned contract, the migration lock, the shared performance budget, and the lockstep deploy. None appears on the architecture diagram. On the right, each service owns its store and one explicit edge remains. The coupling did not disappear — cellar still needs valuations — it became visible, versioned, and negotiable, which is the whole of the cure.

What you honestly give up

Advocates of this rule tend to stop at the benefit. Three real losses, stated plainly.

The join is gone. “Every bottle in this cellar, with its current appraised value, sorted by value, paginated” was one SQL statement. Across a seam it becomes: fetch the bottle IDs from cellar, call valuation with the ID list, join in application code, and — because sorting by a field the other service owns cannot be done before you have it — either fetch everything and sort in memory, or maintain a replicated read model. What was one line becomes infrastructure with a cache-invalidation question attached. This is a genuine cost and the honest response is to notice it during design, not after.

Some transactions are gone. Anything that wrote across the seam atomically is now a saga (module 7), with compensations, observable intermediate states, and reconciliation.

Reporting gets harder before it gets better. The cross-cutting question — “average portfolio value by acquisition year for users who joined in the last two quarters” — touches four services' data and belongs to none of them.

That last one deserves special attention, because it is where the shared database comes back in through the basement. The request always arrives in the same shape: analytics or finance asks for a read-only credential to everyone's database, just for dashboards, promising to touch nothing. Grant it and you have restored the coupling exactly — a migration in any service now breaks a dashboard, and the moment a dashboard is important enough, the dashboards get a vote on migrations. The corrective is that reporting is a consumer like any other and gets a contract: each service publishes an owned export or event feed into the warehouse, the warehouse holds the analytical model, and cross-service questions are answered there. Guide Nº 20 covers what that pipeline looks like; the architectural point here is only that “read-only” is not the same as “uncoupled.”

When it misleads

The rule is about services — units with independent deployment and ownership — not about every module in a codebase. Inside a modular monolith, schema-per-module with no cross-schema joins is the same discipline at zero cost, and it is worth doing precisely because it makes a later extraction mechanical. What is not worth doing is fragmenting a single team's single deployable into separate database instances for the aesthetics of the rule. Ownership and access discipline are the substance; instance count is an implementation detail that occasionally follows.

Autopsy: the distributed monolith

The patient. Veridex, a GRC compliance platform: policy management, control definitions, evidence collection, audit workflow, and reporting for regulated customers. Five services — policy, controls, evidence, audit, reporting — five repositories, five CI pipelines, five container images. On the architecture diagram it is a microservices system, and it was presented as one for three years.

Presenting symptoms. Deploys happen on Thursday evenings, all five together, preceded by a two-hour coordination call. Any single service's release is blocked until the others are ready. The last four incidents each began in one service and manifested in another. Onboarding a new engineer takes a quarter, because understanding any one service requires understanding the shared data model that all five write to. Lead time from merge to production averages nine days.

The finding. One Postgres database, named grc, and all five services hold credentials with read and write on all of it. That single fact produces every symptom above:

  • A controls schema migration — say, splitting control_status into control_status and control_assessment — breaks audit's queries and reporting's views. So migrations must ship simultaneously with every consumer's adaptation. That is the deploy train. The train is not a process failure; it is the correct response to the data coupling, which is why speeding it up has never helped.
  • An evidence bulk import takes a long-held lock on evidence_items. policy queries that same table to render a policy's linked evidence and starts timing out. A customer sees the policy page fail during an import in a service they have never heard of. No fault isolation, in either direction.
  • reporting reads all four other services' tables directly, which means those four cannot rename anything. Over three years, this has produced a schema nobody refactors — columns named for concepts the product abandoned, and one table whose name is a former customer's.

Diagnosis against module 1's matrix: weak boundaries, many deployables — the top-left quadrant. Veridex pays five pipelines, five images, five sets of alarms, network hops between components that used to be function calls, and a distributed debugging problem, and it collects none of the four purchases: it cannot deploy independently (the train), cannot scale independently in any useful way (all five contend on the same database), has no team autonomy (every change is negotiated at the Thursday call), and has no fault isolation (shared locks). All of the invoice, none of the purchases. A well-run modular monolith would strictly dominate it.

Five Veridex services all accessing one database with dense access edges, beside the redrawn version with per-service ownership, contract edges, a warehouse feed, and five independent release arrowsVeridex as found — 5 services, 1 databasepolicycontrolsevidenceauditreportinggrc — 40 tables, 5 principals, read+write17 access edges. The density is the argument: no table has an owner,so no service can migrate, deploy, or fail alone.one Thursday deploy train · 2-hour coordination callVeridex redrawn — one owner per storepolicycontrolsevidenceauditreportingversioned contracts, not tablespolicy.*controls.*evidence.*audit.*warehousedashed = owned change feeds into the warehouse; reporting reads only therefive independent releases — the train is goneSame five services. Same five pipelines. The difference is thateach one can now change its own schema without asking.This is the move from Figure 1.2's top-left square to its top-right one:the invoice was already being paid; only now are the purchases collected.
Figure 5.2 — The autopsy and the redraw. Left: five services and seventeen access edges into one 40-table database, with no owner anywhere — the density is the whole argument, and the Thursday deploy train is its necessary consequence rather than a process failure. Right: each store has exactly one owner, cross-service reads become versioned contracts, reporting is fed by owned change feeds into a warehouse instead of reading four schemas, and five independent release arrows replace the train. Nothing about the service count changed; what changed is whether any of them can move alone.

The redraw, in sequence. The order matters, and it is the opposite of what teams usually attempt.

  1. Assign ownership on paper first. Every table in grc gets exactly one owning service. This is a week of archaeology and produces the actual finding: 31 of 40 tables have an obvious owner, 6 are owned by evidence but written by audit in one workflow, and 3 are genuinely shared reference data (control frameworks, regulation citations) that belong in a new reference service nobody had thought to name.
  2. Cut the reporting reads first. reporting is the highest-fan-in consumer and the easiest to detach because it only reads. Each owning service publishes a change feed to the warehouse; reporting is rebuilt against the warehouse. This alone frees four services to rename columns, and it is achievable in a quarter without touching a write path.
  3. Replace the six cross-written tables with contracts. audit stops writing evidence's tables and calls evidence's API; the sixth case, which needs atomicity, becomes a saga (module 7) or moves wholesale into evidence — this is where you discover that the seam between audit and evidence may simply be in the wrong place, and moving the workflow is cheaper than distributing it.
  4. Split the schemas physically, then dissolve the train. Per-service schemas with per-service roles and no cross-schema grants, so the database enforces what the diagram claimed. The train is not cancelled by decree; it dissolves service by service as each becomes migratable alone. reporting leaves the train at the end of step 2 — the first independent release in three years, and the evidence that the sequence is working.
Anti-pattern: the shared database

Symptom: services described as independent that cannot change a schema alone. Tells: a release train or a coordination call, a migration checklist listing other teams, more than one service's credentials in one database, an ORM model class imported by two services, or a schema whose oldest columns cannot be renamed because “something reads that.”

Corrective: one owner per store, contracts for everyone else — and sequence it ownership-first, reads-before-writes, physical split last. Do not start by improving the deploy train; the train is a symptom, and making it faster removes the pain that was going to fund the actual fix.

Module 06 Decomposition and the strangler fig

Five modules of frame and force. This one is mechanics: where the seams go, how to find where they already are, and how to make one physical without a weekend cutover and a rollback plan nobody has tested.

The two halves are connected by a single principle. Decomposition by capability works because a capability is the unit that changes as a unit — which is module 1's coupling argument arriving with a name. The strangler fig works because it never requires you to be right all at once; every stage is a small bet with a stated exit. If you take one operational habit from this course, take the checkpoint discipline in section three, because it is what converts an architecture decision from a leap into a sequence of reversible steps.

Split by capability, not by layer

A business capability is a noun, its operations, and its data, owned together: cellar management, valuation, purchasing, recommendations. Split along capabilities and a feature request lands inside one service — “let users record the storage temperature of a bottle” touches cellar and stops there.

The alternative that keeps getting proposed is the technical layer: an api-service, a logic-service, a data-service. It has an appealing symmetry and it is the worst common decomposition, because the same feature now requires a change in all three: a new field in the API layer, handling in the logic layer, storage in the data layer — three repositories, three deployments, three reviews, one release order. Layer splits maximize cross-boundary change while presenting as separation of concerns. Module 1 gave you the test: count the change-ripples that cross each boundary. A layer boundary is crossed by nearly every feature; that is what a layer is.

Use the change-path test on any proposed decomposition. Take three real, recent feature requests, trace where each lands, and count the services touched. If the answer is routinely more than one, the seams are in the wrong place, and no amount of good engineering downstream will recover from it.

Positioning: bounded contexts

Domain-driven design calls this a bounded context — a boundary inside which a model's terms have exactly one meaning. It is worth knowing the term because it names the sharpest test available: if “bottle” means a physical object with a location and a condition on one side of the seam, and an asset with a cost basis and an appraisal history on the other, you have found a real boundary rather than drawn one. When one word needs two different models, the seam is between them. The full DDD methodology is out of scope here; that one heuristic does most of the work.

Layer-based decomposition with one feature's change path crossing all three services, beside capability-based decomposition where the same feature lands inside one serviceSplit by technical layerapi-servicelogic-servicedata-serviceFeature: “record a bottle's storage temperature”3 services · 3 repos · 3 deploys · 1 release orderEvery feature crosses every boundary. That is what a layer is.Split by business capabilitycellarvaluationpurchasingrecsSame feature: 1 service · 1 deploy · no release orderEach column owns its API, its logic, and its data.The layers still exist — inside each service, where they cost nothing.The change path is the argument. Trace three real features; count the services each one touches.
Figure 6.1 — The change-path test. The same feature request drawn against two decompositions. Layered services turn one change into three coordinated ones because every feature needs a piece of each layer; capability services absorb it into one column that owns its own API, logic, and storage. The layers have not disappeared in the right-hand picture — they exist inside each service, where crossing them is a function call rather than a release.
Anti-pattern: splitting by technical layer

Symptom: every ticket fans out into two or three services' backlogs and acquires a release order; standups are spent on sequencing; service names are nouns of the software (gateway, orchestrator, persistence) rather than nouns of the business.

Corrective: redraw along business nouns so a feature lands in one service, and let the layers live inside each one. If a layer split is already in production, the migration path is the strangler fig applied per capability — carve out one vertical capability at a time rather than attempting to rotate the whole architecture ninety degrees at once.

Finding the seams that already exist

Boundaries are discovered more than invented. A whiteboard session produces a decomposition that reflects how the people in the room think about the domain; the repository contains evidence about how the system actually changes. Prefer the evidence. Four sources, in rough order of usefulness.

Change co-occurrence. Module 1's measurement: which files and modules move together in pull requests. Six months of history is enough. Clusters in that data are candidate services; a proposed seam that cuts through a dense cluster is a proposed problem.

Data ownership. Which tables does each module write, and which does it only read? A module that exclusively writes a set of tables nobody else touches is already a service in every respect except deployment. A module whose tables are written by three others has no boundary yet, whatever the diagram says.

Org communication lines. Module 4. Where teams already coordinate by contract rather than continuously, a seam exists and is being maintained for free. Where one team spans a proposed seam, that seam is a liability until ownership moves.

Transaction scopes — as constraints, not candidates. This one is different in kind. The others tell you where seams are; transaction scopes tell you where a seam will be expensive. The wine-cellar record-a-purchase transaction spans purchasing, cellar, and valuation; cutting a seam through it means building a saga. That does not forbid the seam, it prices it — and it is a price that belongs in the proposal, not in a surprise ticket in month three.

The wine-cellar evidence for recs, tabulated:

Evidence sourceWhat it says about the recs seam
Co-change9 PRs in the last month touched recs; none of them touched any other module. Over six months, 2 of 47 recs PRs were cross-module, both adding the same facade method.
Data ownershiprecs.* is written only by recs. It reads cellar and catalog data exclusively through their facades, already — a habit the import-linter enforced.
OrgOwned solely by the ML team for two quarters. They already ask before changing the facade, because they know product engineering calls it.
TransactionsNo transaction spans recs and anything else. Recommendations are computed and cached; nothing about them is atomic with a user's write.

Four sources, four agreements. The seam is already there; the only question left is whether a pressure justifies making it physical — and module 3 answered that with a ledger. Contrast the seam between cellar and purchasing, where co-change is moderate, data is separate but a transaction spans both, and one team owns both sides: the evidence is mixed, so the honest conclusion is that it stays a module boundary. A seam contradicted by all four sources is a fantasy, and a seam supported by three of four is a conversation.

The strangler-fig migration

You have decided a seam should be physical. The question is how to get there without a period during which the system does not work.

The big-bang rewrite answers: build the replacement alongside for two quarters, freeze features, then cut over on a weekend. Its risk shape is the problem. All the value arrives at the end, and until then all the risk is carried — you cannot know whether the new implementation is correct under real traffic, because it has never seen any. If the cutover fails, the fallback is the old system plus however many months of divergence, and the divergence is usually why it failed.

The strangler-fig migration answers differently: grow the new implementation around the old one behind a single routing point, moving traffic in stages, until nothing calls the old code and you delete it. The name is from the fig that grows around a host tree and eventually stands on its own. Its shape:

  1. Put a facade in front. A single interception point through which all traffic to the capability flows. In a modular monolith this step is nearly free, because the module facade is already that point — the discipline from module 2 paying its first dividend.
  2. Build the service alongside. It exists, it is deployed, and nothing routes to it.
  3. Move reads: shadow, compare, then serve. The facade calls both, returns the old answer, and logs disagreements. When agreement meets a stated threshold for a stated duration, the facade starts serving the new answer.
  4. Move writes. The new service becomes the owner of its data; the old tables become read-only and then unused.
  5. Shift traffic in percentages, then delete. 10%, 50%, 100% — and then the stage everybody skips: remove the old code path, the feature flag, and the dead tables, with a date on it.

The defining property, which is the whole reason to prefer it: at every checkpoint you can stop, stay indefinitely, or roll back, and the system works. That is not a nice property, it is the property. It means the migration never competes with an incident, never blocks a quarter's roadmap, and never requires the org to hold its nerve for six months. A migration that can be paused is a migration that finishes.

Five-stage strangler-fig timeline for extracting the recommendation service, showing routing state at each stage with safe-to-stop checkpoints and rollback movesExtracting recs — routing state at each stage1 · facade in placefacademonolith recs2 · shadow readsfacademonolith recsrecs serviceanswers compared, old one served3 · reads servedfacademonolith recsrecs servicewrites still in the monolith4 · writes movedfacaderecs serviceowns recs.* store5 · old code deletedfacaderecs serviceflag removed · dead tables droppedsafe to stopsafe to stopsafe to stopsafe to stopdonerollback: revert PRrollback: stop shadowingrollback: flip flag backrollback: re-point writesrollback: none — commitThe invariant: at every vertical slice of this timeline, production is whole and the migration can stop indefinitely.Stage 5 is a real stage with a date. An extraction that never deletes the old path has bought two implementations, not one.
Figure 6.2 — Strangler fig, five stages. Traffic flows through the facade throughout; what changes is where the facade routes. Reads move first under shadow comparison, then writes and data ownership, then the old path is deleted on a scheduled date. Each checkpoint has a rollback that is one flag flip or one revert — which is what makes the whole sequence pausable, and therefore finishable.
Anti-pattern: the big-bang rewrite

Symptom: a long-lived branch, a feature freeze on the old system, a cutover weekend on the calendar, and a rollback plan that has never been executed. Often accompanied by the belief that the new system will be clean because this time the team knows the domain.

Corrective: the strangler fig, even when it is slower in calendar time — because its risk is retired incrementally rather than carried to the end, and because it survives the thing that actually kills rewrites, which is the twelve months of business change the old system absorbed and the branch did not.

Extracting recs, step by step

The plan, at the depth a real proposal needs.

Stage 0 — the routing point. There is nothing to build. Every caller already goes through recs.facade.recommend(user_id, n), because the import-linter has forbidden anything else since the module was created. That is the modular monolith's dividend arriving: the seam is already the single interception point, so stage 1 is a code review, not a project. In a system without that discipline, stage 1 is the largest stage — funnel every caller through one function first, and do nothing else until that is true.

Stage 1 — build alongside. The recs service is deployed with its own image, pipeline, alarms, and on-call rotation (ML team). It reads the monolith's recs.* schema directly during transition. That is a deliberate, temporary violation of module 5's rule, and it is acceptable only because it is time-boxed, one-directional (the service reads; the monolith still writes), and closed at stage 4. Write it in the plan as debt with an owner and a stage number, because otherwise it becomes the permanent shared database that module 5 performs autopsies on.

Stage 2 — shadow and compare. The facade calls both implementations, returns the monolith's answer, and logs disagreements with the inputs. This is where the extraction earns its keep: on day three, the shadow comparison shows a 2% disagreement rate, concentrated in users with more than 400 bottles. Cause: the service's feature loader paginates at 500 and the monolith's did not, so large cellars were being scored on a truncated inventory. Stop the clock, fix it, resume the shadow period. That bug would have been a silent quality regression discovered by a customer complaint six weeks after a big-bang cutover, and here it cost three days and no user saw it.

Stage 3 — serve reads. Checkpoint criterion, stated numerically in advance: shadow agreement ≥ 99.5% over 7 consecutive days, and service p99 under 200 ms. Then the facade serves the new answer behind a percentage flag: 10% of users for 48 hours, 50% for 48 hours, 100%. Rollback at any point is one flag change, taking effect in seconds. The monolith's implementation stays in place, still exercised by the shadow in reverse if you want the safety.

Stage 4 — move writes and data. The service takes ownership of its store. Backfill the existing recs.* data into the service's own database, then switch the write path so the service is the only writer; the monolith's tables go read-only, then unused. The transition is verified by comparing row counts and a sample of computed profiles before and after the switch, and by the fact that stage 2's shadow infrastructure is still available to re-enable.

Stage 5 — delete, on a date. Remove the monolith's recs implementation, the percentage flag, the shadow-comparison code, and the old tables. This stage is scheduled — “14 days after 100% traffic with the error budget intact” — because the alternative is a system that permanently maintains two implementations of recommendations, and pays for both. An extraction that never reaches stage 5 has not simplified anything; it has bought an additional service and kept the old one.

Anti-pattern: premature extraction

Symptom: a service split before the domain was understood — its contract changes every sprint, its consumers pin versions, and design discussions keep concluding that the boundary is “slightly wrong” in a way nobody can fix cheaply. The tell is a service younger than the product's last significant pivot.

Corrective: boundaries you do not understand yet stay module boundaries, because those are cheap to move — a refactor, not a migration and a deprecation cycle. Extraction freezes a contract; only freeze one you have watched hold still for a while. This is the same reasoning as module 8's reversibility ranking, arriving early.

Module 07 Wiring services together

Two seams in the wine-cellar system are now physical. recs was extracted in module 6. valuation followed two months later, on its own evidence: the market-data ingest pipeline had grown to a nightly job that needs eight times the memory of a web worker and runs on a schedule nothing else shares, and the data team took sole ownership after module 4's diagnosis. The ledger passed on scaling divergence and cadence conflict; the bills were the same shape as module 3's, plus one that this module is about.

Because with valuation outside, the “value a cellar” operation — reappraise every bottle at current market, update cost bases, refresh the insured total — no longer fits inside one BEGIN…COMMIT. Module 2 showed that transaction as a genuine asset. This module is where you pay for it, and where you decide, deliberately, what a user sees in the moments when the system is halfway through.

Synchronous or asynchronous, by who needs the answer

One rule survives contact with real systems: call synchronously when the caller cannot proceed without the answer; publish an event when the caller is done and others merely care.

Charging a card before confirming an order is synchronous — the confirmation is a lie without the charge result. Rendering the cellar page needs the bottle list synchronously and the recommendations only optimistically, which is why module 3's fallback works. And when a user adds a bottle, cellar is done once the bottle is stored; that valuation wants to appraise it, recs wants to update the taste profile, and notifications may want to mention it are all facts about other services' interests, not about the user's request. So cellar publishes bottle.added and returns.

The failure mode when this rule is ignored is the synchronous relay, and it deserves its arithmetic. Suppose cellar calls valuation, which calls recs, which calls notify, all in series, all on the user's request path. Latency is the sum of the line: four hops at ~1 ms of transport plus each service's own work — but the real problem is availability. If each service is independently available 99.9% of the time, the chain's availability is 0.999⁴ ≈ 0.996: roughly 35 hours of user-visible downtime per year, versus about 8.8 hours for one service — some 26 hours attributable purely to the chain shape. You did not deploy anything less reliable; you multiplied.

The bottle-added flow drawn twice: a four-service synchronous relay with multiplied availability, and an event publication with three independent consumersSynchronous relay — the user waits for all of itcellarvaluationrecsnotify99.9% · 15 ms99.9% · 40 ms99.9% · 25 ms99.9% · 30 msavailability = 0.999⁴ ≈ 0.996 → ~35 h/year down, vs ~8.8 h for one servicelatency = 110 ms of work + ~3 ms of hops, all on the user's requestEvent publication — the user waits for one writecellarbottle.addedvaluationrecsnotifyappraises on its own clockupdates the taste profilemay or may not notifyuser response: 15 msavailability: 99.9%A consumer being down now means staleness, not an error. That is the trade: you exchange immediacy for independence, and you should only take it where immediacy was never required.
Figure 7.1 — Relay versus event. The same “bottle added” flow. Above, four services in series on the user's request path: latency is the sum and availability is the product, so four 99.9% services yield ~99.6% and about 35 hours of downtime a year, roughly 26 of them created purely by the shape. Below, cellar stores the bottle, publishes one event, and returns in 15 ms; the three consumers react on their own clocks, and a consumer being down produces staleness rather than a failed user request.
Anti-pattern: the synchronous call-chain relay

Symptom: one user request fans through three or more services in series; an incident in a leaf service pages the team that owns the entry point; the p99 of the whole is the sum of everyone's p99s, and no single team can improve it.

Corrective: collapse the chain. Either orchestrate from one point so the calls are parallel and independently failable, or move the steps nobody is waiting on to events. The test is per-hop: at this hop, does anyone need the answer before responding to the user? If not, it does not belong on the request path.

Cross-reference — Guide Nº 17

Everything about how the event gets delivered — broker choice, at-least-once versus at-most-once, ordering guarantees, consumer groups, dead-letter handling, and why exactly-once is an engineered illusion rather than a delivery mode — belongs to Guide Nº 17 and is deliberately not reopened here. The only fact this module borrows: delivery is at-least-once in practice, so every event consumer must be idempotent. Design your consumers so that processing bottle.added twice produces the same state as processing it once, and the rest of that guide's machinery becomes an implementation detail rather than a correctness dependency.

Contracts at the boundary

Inside one deployable, an interface is a convention among people who ship together. Across a physical seam, the API is the relationship — the only thing the consumer may rely on and the only thing the provider is obliged to keep.

Three properties make a contract carry that weight. It is explicit: the fields, their types, their nullability, and the error cases are written down, and anything not written down is not promised. It versions: additive changes ship freely, breaking changes get a new version and a deprecation window, and the provider knows who its consumers are. Someone pays for compatibility: the window during which the provider supports both versions is the actual price of independent deployment — module 3's first purchase, and this is where it is collected. A team that refuses to maintain compatibility windows has not bought independent deployment; it has bought a coordination meeting with worse latency.

The mechanism that keeps this honest is the consumer-driven contract test: each consumer publishes the subset of the contract it actually relies on, and the provider's pipeline runs those expectations. A field removal then fails the provider's build rather than the consumer's production. That inversion is the point — it moves the consequence to the party making the change, which is the only arrangement that scales past two teams.

From your other life — privity

You already have the right instinct for this from contract law. A party may rely on the terms of the agreement; it may not rely on the counterparty's internal practices, however consistently observed. If a supplier has shipped on Tuesdays for three years and the contract says fourteen days, Wednesday is not a breach. Same here: recs may rely on cellar's published response schema, and may not rely on the fact that the array happens to come back sorted by acquisition date, or that an undocumented internal_grade field is present. “It happens to work today” is not a term of the agreement, and when it breaks, the fault is the reliance, not the change. Guide Nº 07 covers the mechanics — versioning schemes, deprecation headers, error taxonomies — in full.

The end of easy transactions: sagas

Module 2's BEGIN…COMMIT is unavailable across a physical seam. What replaces it is a saga: a sequence of local transactions, each committed in its own service, each paired with a compensating action that undoes its business effect if a later step fails.

The word “undoes” is doing careful work. A compensation is not a rollback. A rollback discards uncommitted changes that nobody has seen. A compensation is a new, forward business action against state that has already committed and may already have been observed, acted on, or emailed to someone: release the reservation, refund the charge, send the correction. There is nothing left to roll back, which is why the design question is never “how do we roll this back” but “what is the business action that makes this right.”

Take the wine-cellar commission-purchase flow, where a user commissions the platform to acquire a specific lot at auction. Three services: purchasing reserves the lot with the merchant, payments (the boundary around the external processor, entering the cast here) charges the stored card, and notify sends the confirmation. Sequencing is a design decision, and this ordering is deliberate — the reservation is reversible, the charge is reversible with a refund, and the confirmation email is not reversible at all, so it goes last.

A three-service saga shown on the happy path and with a mid-sequence charge failure triggering a compensating reservation release, contrasted with the single transaction the monolith performed for the same operationHappy pathpurchasingpaymentsnotify1. reserve lot 44172. charge €2,4003. confirm by emaillast — cannot be unwoundMid-sequence failure — the charge declinespurchasingpaymentsnotify (not reached)reserved · committedcard declined · nothing chargedcompensate:release lot 4417merchant inventory restoredWhat the user sees, by designduring: “commission pending — reserving lot”after: “payment declined, lot released,nothing was charged”What module 2 got for freeBEGINreserve · charge · record — three writes, one unitROLLBACK on any failure — no compensation code, no observable middleone process,one database
Figure 7.2 — A saga and the transaction it replaced. Top: reserve, charge, confirm, in an order chosen so the irreversible step is last. Middle: the charge declines after the reservation has committed, so the reservation is compensated by a forward action — releasing lot 4417, which restores the merchant's inventory — and the user is told plainly what happened. Bottom: the monolith's version of the same operation, one bracket, no compensation code and no observable intermediate state. The saga is not worse engineering; it is the same operation with the free atomicity removed and the missing guarantees rebuilt by hand.

Two properties make this saga correct rather than merely plausible. The compensations genuinely compensate: releasing the reservation restores the merchant's inventory to exactly the state it was in, and no third party has acted on the reservation in the interim because reservations are not visible outside the platform. The irreversible step is last: once the confirmation email is sent it cannot be recalled, so nothing after it may fail. If a fourth step existed that could fail after the email, the email would have to move — or the whole design would need a different seam.

Consistency as a product decision

Eventual consistency is usually introduced as a technical property. Its practical meaning is narrower and sharper: there is now a moment, of some duration, in which a user can observe the system halfway through an operation — and what they observe is a design decision that somebody is going to make, explicitly or by accident.

Concretely: a user triggers “value my cellar.” valuation reappraises 240 bottles, writes cost-basis updates, and refreshes the insured total. It takes eleven seconds. For those eleven seconds, if the cellar page shows an insured total that no longer matches the sum of the visible bottle values, the user has found a bug — and they are right, in the only sense that matters, because the interface presented a stale number as a current one. Show “revaluation in progress · totals as of 14:02” instead and the same underlying state is now honest, comprehensible, and unremarkable. Nothing changed in the data layer. The bug was in what the product claimed.

This generalizes into the design obligation: for every saga, enumerate the intermediate states and specify what the user sees in each. If you cannot describe a mid-saga state in a sentence a customer-support agent could read aloud, you have not finished the design.

Sometimes the honest conclusion is that the seam is wrong. If the compensations cannot genuinely compensate, the transaction is load-bearing and the boundary must move rather than the invariant. Compliance is the clearest case: in Veridex, an auditor's attestation is a signed statement about a set of evidence at a point in time. There is no forward business action that un-attests — a retraction is a new, separately meaningful artifact, and the regulator has seen both. So the attestation and the evidence-set snapshot it references must commit together, which means they belong in one service. That is not a failure to find a clever saga; it is the correct reading of a domain where an observed state has legal consequences and cannot be walked back.

Two positioning notes, and no more

Two-phase commit (2PC) is the textbook alternative: a coordinator asks every participant to prepare, then tells them all to commit. It works, and it is effectively dead across services, because during the prepare phase every participant holds locks and waits, so the availability of the whole becomes the availability of the least available part and a coordinator failure can leave participants blocked indefinitely. You gave up a distributed transaction to buy independent failure; 2PC buys it back at a higher price. It remains reasonable within one service across its own resources.

Event sourcing and CQRS are a further pattern family aimed at exactly this module's problems — storing state as an append-only sequence of events and separating the write model from read models. They genuinely help with cross-service history, audit, and read-model construction. They also add real complexity (versioning events forever, rebuilding projections, reasoning about replay) and belong to their own guide. Named, placed, out of scope.

Anti-pattern: assuming distributed transactions still exist

Symptom: a design document that uses the word “transactionally” across a service boundary; a code review comment asking why the framework cannot just roll the other service back; a plan whose failure handling is “retry until it works” with no compensating action named.

Corrective: a saga with explicitly named compensations and specified observable intermediate states — or, when the compensations cannot genuinely compensate, do not cut the seam there. Those are the two options, and “we will handle the edge cases later” is not a third.

Module 08 The decision framework

Seven modules of frame, force, and mechanics. This one converts them into something you can run on a Tuesday when someone proposes a split and everyone looks at you.

The framework is opinionated, and it should be: the course has earned the opinion. Start with a modular monolith with CI-enforced seams. Make a seam physical only when a named pressure signal is present with evidence. Prefer the strangler fig always. Never share a database across a service boundary. Hold boundaries you do not yet understand as module boundaries, because those are cheap to move. “It depends” is an acceptable answer only when it is immediately followed by what it depends on — and by this point in the course, you can name those things.

The default and the triggers

The default: a modular monolith with seams enforced in CI. Not as a compromise or a starter architecture, but as the correct answer until something specific changes it — because it gets you every design benefit at none of module 3's invoice, and it keeps every boundary cheap to move while the domain is still teaching you where the boundaries are.

The triggers. Make a seam physical when one of five pressure signals is present with evidence. Each corresponds to a purchase from module 3, which is why a signal is what makes a purchase pay out.

Pressure signalEvidence that proves itPurchase it collects
Scaling divergenceMeasured load profiles differing by an order of magnitude across the seam, or resource shapes that differ in kind (GPU/memory vs. request-bound)Independent scaling
Team contentionCo-ownership friction: merge-conflict rate, features needing two teams in one sprint, an interface accumulating redundant methods (module 4)Team autonomy
Release-cadence conflictOne module's releases repeatedly delayed by another's risk profile — count the waits, with datesIndependent deployment
Fault-isolation requirementA named failure domain that must not take the rest down, plus a caller willing to build the timeout and fallbackFault isolation
Technology mismatchA genuinely different runtime need — inference hardware, a different language ecosystem, a compliance boundary requiring separate infrastructureTechnology autonomy

Zero signals means stay, and revisit when evidence appears. A split justified by zero signals is module 1's résumé line, whatever the document calls it.

Decision flowchart for whether to make a module boundary physical, gating on seam cleanliness, then on five evidenced pressure signals, then on the boundary ledger, ending in a strangler-fig extraction“We should split X into a service”Gate 1 — is X a clean module boundary today?one facade · owns its data · enforced in CI · low co-changeno →strengthen the seamfirst (m2), then returnyesGate 2 — which pressure signals are present, with evidence?scaling divergencemeasured, ≥10×team contentionconflict + sprint datacadence conflictcount the waitsfault isolationnamed domaintech mismatchruntime differs in kindzero signals → stayrevisit on evidence, not on a date≥1 signal → run the m3 ledgerpurchases + pay-out conditions vs. itemized invoiceGate 3 — is the ledger favorable?no → stay, and recordwhich bill was the blockeryes → strangler-fig extraction (m6)one seam at a time · checkpoints · dated deletionEvery “no” is acomplete answer,not a deferral.
Figure 8.1 — The runnable gate. Three gates in order. First, is the boundary clean today — if not, strengthen it inside the deployable and come back, because distributing an unclean seam is module 1's fatal move. Second, which of the five pressure signals is present with evidence; zero means stay and revisit when evidence appears, not on a calendar. Third, does module 3's ledger come out favorable — and if not, record which bill was the blocker, because that is the thing that might change. Only then does an extraction begin, and it begins as a strangler fig.

Reversibility and the decision record

Two decisions with the same expected value are not equally good if one is far more expensive to undo. Rank the moves in this course by cost-to-reverse:

rename a module < move code between modules < merge two services < split a service < un-share a database

The first two are pull requests. Merging two services is a project with a clear end. Splitting a service creates a contract, a deprecation obligation, and consumers you will have to negotiate with. Un-sharing a database — module 5's autopsy — is a multi-quarter program that touches every consumer you can find and several you cannot.

The operating rule follows: while the evidence is incomplete, hold the decision in its cheapest reversible form. A module boundary is a service boundary held in escrow. It expresses the same design belief, costs nothing, and can be moved on a Thursday when the domain teaches you something. Extraction converts that belief into a frozen contract and buys you the pressure-signal purchases; do it when the evidence is in, and not before.

The instrument for all of this is the architecture decision record — one page, written when the decision is made, containing the pressure evidence, the ledger, the bills accepted, the rollback path, and a revisit condition. The revisit condition is the part people skip and the part that matters most, because architecture decisions are correct relative to conditions, and conditions move. “We split recs because scaling diverged 75× and the ML team's cadence conflicted” is a decision a future engineer can evaluate: they can check whether those two things are still true. “We use microservices” is not evaluable by anyone, ever.

From your other life — GRC

This is a control-decision memo, and it serves the same audit function: the finding, the evidence, the compensating controls accepted, the residual risk, and the review date. You already know why the review date is load-bearing — a control approved against 2023's threat model and never revisited is not a control, it is a document. Architecture decision records fail the same way and for the same reason, and the discipline transfers directly: write the condition under which this decision should be re-litigated, and put a date on the review.

The anti-pattern field guide

The course's ten anti-patterns, consolidated. Enumeration is genuinely the clearest structure here — this is a lookup table, meant to be used when you are looking at a real system and trying to name what you are seeing.

NameSymptom in the wildCorrectiveFull treatment
Microservices as a default or résumé lineMore boxes than engineers; asked which pressure motivated a split, the answer is a shape rather than a measurementName the signal and produce the evidence, or stay in one deployablem1
The distributed monolithSeparately deployed services that must ship together; a release train or a coordination callAssign data ownership first; contracts replace shared tables; the train dissolves service by servicem1, m5
“The monolith made us do it”A migration proposal blaming the deployment model for cross-module joins, missing ownership, and direct internal importsAttribute the rot to missing enforcement; fix boundaries where they are cheap, then decide about distributionm2
“The network is reliable” thinkingA new remote call with no timeout, no fallback, and a retry wrapper with no idempotencyAll three at the moment the call is written; treat their absence like an unhandled exception in reviewm3
Ignoring Conway's lawEvery feature needs two teams; the retrospective blames communication and adds a sync meetingMove the seam or move the ownership; stop adding coordination to a structural mismatchm4
The shared database“Independent” services that cannot migrate a schema alone; multiple services' credentials in one databaseOne owner per store, contracts for everyone else; sequence ownership → reads → writes → physical splitm5
Splitting by technical layerEvery ticket fans into two or three backlogs with a release order; service names are software nouns, not business nounsRedraw along capabilities so a feature lands in one service; let layers live inside eachm6
Big-bang rewriteA long-lived branch, a feature freeze, a cutover weekend, an untested rollback planStrangler fig, even when slower — risk retired incrementally, and it survives the business changing underneath itm6
Premature extractionA service younger than the last product pivot whose contract changes every sprint and whose consumers pin versionsKeep unclear boundaries as module boundaries; they are cheap to move, and extraction freezes a contractm6
The synchronous call-chain relayOne request through three or more services in series; a leaf incident pages the entry-point team; nobody can improve the p99 aloneCollapse the chain: orchestrate at one point, or move to events wherever nobody is waiting for the answerm7
Assuming distributed transactions still existA design document that says “transactionally” across a service boundary; failure handling that is retry-until-it-worksA saga with named compensations and specified intermediate states — or do not cut that seamm7

Two notes on using this table. First, these co-occur, and the order of treatment matters more than the count: shared data is nearly always the first corrective, because until ownership is assigned you cannot know which other fixes are even available. Second, several of these are the same underlying error at different points in the process — microservices-as-default, premature extraction, and the big-bang rewrite are all versions of committing to an expensive irreversible move before the evidence justifies it.

The wine-cellar system, retrospectively

Run the whole arc against Figure 1.2 one last time.

It was born in the bottom-right. Seven modules, narrow facades, a schema namespace each, an import-linter contract in CI from the first month. That was a choice made with two engineers and no scaling problem, and it looked like overhead at the time. It was not: it is why every later step in this story was cheap.

It moved up one square, once, on evidence. recs was extracted after two signals appeared and were measured — a 75× load divergence with an idle 1.2 GB model replicated eight times, and an ML team whose weekly release cadence was waiting on a fortnightly train. The ledger passed. The extraction took seven weeks, of which four were checkpoint windows, and the shadow-compare caught a truncated-inventory bug that would otherwise have degraded recommendations silently for the platform's most engaged users.

It moved up once more, later, on different evidence. valuation followed on its own signals — an ingest pipeline needing eight times the memory of a web worker on a schedule nothing else shared, and the data team taking sole ownership after module 4's co-ownership diagnosis. That extraction cost a saga, and the saga's fourth step only partially compensates, which is recorded in the ADR rather than discovered in an incident.

And record-a-purchase was never cut. purchasing, cellar, and their single transaction remain inside one deployable, five years in, because no pressure signal ever justified it: one team owns both, the load profiles match, the release cadences are the same, and the atomicity is worth real money. That is the framework producing a negative answer repeatedly and correctly — and a negative answer, recorded with its evidence, is a decision, not an absence of one.

The counterfactual, run honestly. Suppose the same product had been built microservices-first on day one: seven services, seven pipelines, seven stores, operated by two engineers. Every one of the domain lessons that arrived over the following two years — that valuation and cost basis are one concept, that label recognition and recommendations are different capabilities despite both being “the ML stuff,” that purchasing and cellar share a transaction that matters — would have arrived as a contract migration rather than as a refactor. The team would have spent its first year paying module 3's invoice for purchases it could not yet collect, and its second year discovering that several seams were wrong in ways that were now expensive to fix. The architecture would have been fashionable and the product would have been slower.

The load-bearing idea

The question was never monolith or microservices. It was: where do the seams in this system belong, and which of them am I ready to pay for? Draw them early, because they cost nothing and they are what makes everything else possible. Enforce them, because an unenforced seam is a comment. Make them physical one at a time, on evidence, and write down what the evidence was — so that when someone asks in three years why this system looks the way it does, the answer is a decision with a date on it rather than a shrug and a shape.

Concept index

Coupling
The degree to which a change in one part of a system forces changes in another.
Cohesion
The degree to which things that change together live together.
Module boundary
A design-level seam — interface, ownership, visibility — that costs nothing at runtime to maintain.
Deployment boundary
A physical seam between separately deployed units, paid for with a network hop and operational overhead.
Big ball of mud
A system whose internal boundaries have eroded until any code can touch any code and any query any table.
Distributed monolith
Separately deployed services so coupled — typically by shared data or lockstep releases — that they must change and ship together.
Monolith
A system delivered as a single deployable, with one call stack, one transaction scope, and one release.
Modular monolith
A single deployable with strongly enforced internal module boundaries — the design benefits of services without the network tax.
Seam enforcement
The mechanism (visibility rules, import linting, schema ownership) that makes a module boundary fail CI instead of being a suggestion.
Microservices
An architecture of independently deployable services, each owning a business capability and its data.
Network hop
The serialize–transmit–queue–execute–respond path that replaces an in-process call at a physical boundary.
Partial failure
The condition, unique to distributed systems, in which some components of one logical operation fail while others succeed.
Ambiguous timeout
A remote call's fourth outcome: no response, with the work's fate unknowable from the caller's side.
Fault isolation
Containing a component's failure so callers degrade deliberately instead of failing with it — a purchase that pays only if callers honor it.
Correlation ID
An identifier propagated across service boundaries so one user action can be reassembled from many services' logs.
Conway's law
Systems mirror the communication structure of the organizations that build them; interfaces form where communication is expensive.
Inverse Conway maneuver
Reshaping team ownership to match a target architecture so the organizational force pulls the system toward the seams you want.
Team cognitive load
The limit on how much system a team can own responsibly — a sizing constraint on service boundaries.
Shared database
Two or more services reading and writing one schema — an unversioned, invisible contract that welds their deploys and migrations together.
Database-per-service
The ownership rule: each data store has exactly one owning service, and all other access goes through that owner's contract.
Data ownership
The assignment of each store or table to the single service entitled to write it and obligated to serve it.
Business capability
A noun, its operations, and its data owned together — the unit along which systems decompose cleanly.
Bounded context
Domain-driven design's term for a boundary within which a model's terms have one consistent meaning.
Strangler-fig migration
Incremental extraction behind a facade — build alongside, shift traffic in stages, delete the old — safe to stop at every checkpoint.
Facade (routing point)
The single interception point in front of a capability through which traffic can be shifted between old and new implementations.
Synchronous integration
Request/response across a boundary, used when the caller cannot proceed without the answer.
Asynchronous integration
Event-based integration, used when the producer is done and consumers react on their own clocks.
Contract (service)
The published interface a consumer may rely on — the entire relationship between services once the seam is physical.
Saga
A sequence of local transactions across services, each with a compensating action, replacing the distributed transaction that no longer exists.
Compensating action
A forward business action (release, refund, correct) that undoes a committed saga step's effect — not a rollback.
Eventual consistency
The state model in which cross-service data converges after a delay, making the mid-saga moment observable and a product decision.
Two-phase commit (2PC)
The coordinator-based distributed transaction protocol, effectively dead across services because it couples everyone's availability to everyone's.
Architecture decision record
A short document capturing a decision's evidence, accepted costs, and revisit condition, so future engineers can tell if it still holds.
Pressure signal
Named, evidenced justification for making a seam physical: scaling divergence, team contention, cadence conflict, fault isolation, tech mismatch.

This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.