Drawing Boundaries · Nº 24
A field guide to system architecture
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.
“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.
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.
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.
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.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.
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.
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.
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.
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.
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.
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.
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.
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.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.
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.
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 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.
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.
| Mechanism | What it catches | What it costs | Where it leaks |
|---|---|---|---|
Language visibility (package structure, explicit __all__ / __init__ exports, internal subpackages) | Casual reaching-in; makes the facade the obvious path | Near zero; a naming and layout convention | Python 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 name | A config file and a build step; occasional argument about exemptions | Exemptions granted “temporarily” and never removed — the single most common failure |
| Schema ownership: one namespace per module, no cross-namespace joins | The coupling that hides in SQL, which import rules never see | Some queries become two calls or a facade method | Reporting 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 shape | Mapping code at the boundary; some duplication of dataclasses | A “shared types” package that quietly becomes a second shared database |
| Code ownership rules requiring a facade owner's review to change it | Silent widening of the public interface | Review latency on interface changes — which is the point | Nothing, 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.
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 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.
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.
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?
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.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.
| Operation | Order of magnitude | Notes |
|---|---|---|
| In-process function call | ~10–100 ns | No serialization, no copy of anything large |
| JSON serialize + deserialize, KB-scale payload | ~0.1–1 ms | Paid twice per round trip — request and response |
| Same-AZ round trip | ~0.5–1 ms | Excludes remote processing time; TLS handshake adds more on a cold connection |
| Cross-region round trip | ~30–100 ms | Speed 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.
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.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.
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 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.
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.
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.
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.
| Purchase | Pays out? | Evidence |
|---|---|---|
| Independent scaling | Yes | Catalog 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 autonomy | Yes | The 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 deployment | Yes, conditionally | recs 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 isolation | Only if built | Nothing 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.
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.
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.
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.
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.
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.
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.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.
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.
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.
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.
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.
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.
cellar still needs valuations — it became visible, versioned, and negotiable, which is the whole of the cure.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.”
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.
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:
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.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.
The redraw, in sequence. The order matters, and it is the opposite of what teams usually attempt.
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.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.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.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.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.
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.
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.
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.
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.
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 source | What it says about the recs seam |
|---|---|
| Co-change | 9 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 ownership | recs.* is written only by recs. It reads cellar and catalog data exclusively through their facades, already — a habit the import-linter enforced. |
| Org | Owned solely by the ML team for two quarters. They already ask before changing the facade, because they know product engineering calls it. |
| Transactions | No 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.
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:
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.
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.
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.
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.
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.
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.
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.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.
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.
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.
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.
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.
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.
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-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.
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.
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: 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 signal | Evidence that proves it | Purchase it collects |
|---|---|---|
| Scaling divergence | Measured 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 contention | Co-ownership friction: merge-conflict rate, features needing two teams in one sprint, an interface accumulating redundant methods (module 4) | Team autonomy |
| Release-cadence conflict | One module's releases repeatedly delayed by another's risk profile — count the waits, with dates | Independent deployment |
| Fault-isolation requirement | A named failure domain that must not take the rest down, plus a caller willing to build the timeout and fallback | Fault isolation |
| Technology mismatch | A genuinely different runtime need — inference hardware, a different language ecosystem, a compliance boundary requiring separate infrastructure | Technology 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.
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.
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 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.
| Name | Symptom in the wild | Corrective | Full treatment |
|---|---|---|---|
| Microservices as a default or résumé line | More boxes than engineers; asked which pressure motivated a split, the answer is a shape rather than a measurement | Name the signal and produce the evidence, or stay in one deployable | m1 |
| The distributed monolith | Separately deployed services that must ship together; a release train or a coordination call | Assign data ownership first; contracts replace shared tables; the train dissolves service by service | m1, m5 |
| “The monolith made us do it” | A migration proposal blaming the deployment model for cross-module joins, missing ownership, and direct internal imports | Attribute the rot to missing enforcement; fix boundaries where they are cheap, then decide about distribution | m2 |
| “The network is reliable” thinking | A new remote call with no timeout, no fallback, and a retry wrapper with no idempotency | All three at the moment the call is written; treat their absence like an unhandled exception in review | m3 |
| Ignoring Conway's law | Every feature needs two teams; the retrospective blames communication and adds a sync meeting | Move the seam or move the ownership; stop adding coordination to a structural mismatch | m4 |
| The shared database | “Independent” services that cannot migrate a schema alone; multiple services' credentials in one database | One owner per store, contracts for everyone else; sequence ownership → reads → writes → physical split | m5 |
| Splitting by technical layer | Every ticket fans into two or three backlogs with a release order; service names are software nouns, not business nouns | Redraw along capabilities so a feature lands in one service; let layers live inside each | m6 |
| Big-bang rewrite | A long-lived branch, a feature freeze, a cutover weekend, an untested rollback plan | Strangler fig, even when slower — risk retired incrementally, and it survives the business changing underneath it | m6 |
| Premature extraction | A service younger than the last product pivot whose contract changes every sprint and whose consumers pin versions | Keep unclear boundaries as module boundaries; they are cheap to move, and extraction freezes a contract | m6 |
| The synchronous call-chain relay | One request through three or more services in series; a leaf incident pages the entry-point team; nobody can improve the p99 alone | Collapse the chain: orchestrate at one point, or move to events wherever nobody is waiting for the answer | m7 |
| Assuming distributed transactions still exist | A design document that says “transactionally” across a service boundary; failure handling that is retry-until-it-works | A saga with named compensations and specified intermediate states — or do not cut that seam | m7 |
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.
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 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.
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.