Ch 13 / 24 Decomposition and Where the Seams Go 0/0 exercises Exercises ↓

AmouAI Hub/Courses/Software Engineering for AI Engineers/Chapter 13

Part 3 · Designing System Architectures · Chapter 13

Decomposition and Where the Seams Go

Splitting a system into services converts a class of compile-time error into a class of production incident. That is a real trade and sometimes a good one. What decides it is not scale, or fashion, or how many teams you have — it is whether you have found a boundary that things genuinely do not cross.

Reading
Fowler, MonolithFirst · Conway (1968)
Focus
Seams, not nouns
Modes
Architecture · JS · choice
Exercises
12

By the end of this chapter you can

  1. Place four architectures on the axis that actually distinguishes them
  2. Name the four things a network hop costs, and price each one
  3. Find a seam from change patterns rather than from a domain model
  4. Defend a modular monolith without apologising for it
  5. Use Conway’s law as a design input rather than as a joke
  6. Compute the availability of a call chain, and be surprised by it

1Four architectures on one axis

The distinctions people argue about — how many repositories, how many containers, whether there is a message broker — are downstream of one question: how far apart can two changes be deployed?

ArchitectureTwo changes can be deployed…You getYou pay
MonolithOnly togetherOne deploy, one log, one transaction, one place to lookEvery change is coupled to every other change’s release
Modular monolithOnly together, but they cannot break each other at compile timeEnforced boundaries with none of the network costDiscipline. The boundaries are conventions unless something checks them
ServicesIndependently, if the contract holdsTeams that can ship without coordinatingNetwork calls, partial failure, distributed debugging, no shared transaction
Serverless functionsIndependently, per functionDeployment granularity at its finest, and elastic costAll of the above, plus cold starts and a lot of small pieces to reason about

Read down the “you pay” column and notice that it is cumulative. Every step to the right buys exactly one thing — the ability to deploy two changes separately — and pays for it with everything in the columns above. That is not an argument against splitting; it is an argument for knowing what you are buying, because teams routinely pay the price without needing the thing.

The honest test is a question about your last three months: how often did two changes need to ship at different times, and could not? If the answer is “never, we deploy four times a day and nobody waits”, deployment independence is not a problem you have, and buying it is buying an answer to someone else’s question.

2What a network hop costs

An in-process function call and a service call look similar in the code and are not the same thing at all. Four differences, each with a number attached.

Availability multiplies. This is the least intuitive and the most important. If a request needs three services in sequence and each is up 99.9% of the time, the request succeeds 99.9%³ = 99.7% of the time. Ten services in a chain at 99.9% each gives 99%, which is seven hours of downtime a month. Nobody signed up for that; it is arithmetic that happened while people were drawing boxes.

Latency adds, and tails compound. A datacentre round trip is around half a millisecond, which sounds free. The problem is the tail: if each of five services has a p99 of 50 ms, the chance that a request avoids all five slow paths is 0.99⁵ = 95%, so roughly one request in twenty hits at least one p99. Your p95 is now somebody else’s p99.

Debuggability collapses without new tools. In one process, a failure is a stack trace. Across services it is five log files with unrelated timestamps and no shared identifier, unless you built distributed tracing first. Chapter 22 is about building it; the point here is that it is a prerequisite for the split, not a follow-up.

Transactions stop existing. This is the one that cannot be bought back. Two writes in one process are one transaction; two writes across a service boundary are two transactions with a gap in the middle, and the gap is where the money goes missing. Everything people build to compensate — sagas, outbox tables, reconciliation jobs — is a way of living without the thing you gave up, not a way of getting it back.

Two things are worth extracting from that simulation. The first is that an optional dependency is a design decision, not a property of the dependency — the recommendation service is optional because somebody wrote the code that renders the page without it, and that code is the entire difference between a degraded page and a 500. The second is that a shared database is a shared fate: no arrangement of services above it changes what happens when it goes down, which is why a split that leaves one database underneath has bought deployment independence and no availability at all.

3Finding a real seam

The most common way to split a system badly is to split it along its nouns. Invoices, customers, payments — each becomes a service, and every request now touches three of them.

A noun is not a seam. A seam is a place where things genuinely do not need to cross: where the data on one side is not needed to answer a question on the other, where a change to one side does not imply a change to the other, and where the two sides can be wrong about each other for a few seconds without anybody being harmed.

Three tests, in increasing order of usefulness:

TestAskWhat a good seam looks like
TransactionDo these two things ever need to be written atomically?No. If yes, the boundary between them will be reinvented as a saga, badly
ChangeLook at the last 200 commits. Which files change together?Files on either side of the seam rarely appear in the same commit
StalenessCan one side be a few seconds out of date about the other?Yes, and you can say what “a few seconds” costs when it happens

The change test is the strongest because it is empirical and cheap. Your version control history is a record of which parts of the system actually move together, and it does not care about anyone’s domain model. Two modules whose files appear in the same commit 80% of the time are one module wearing two names, and splitting them into services means every feature becomes a cross-team release. Two modules that have never appeared in the same commit are already separate; the boundary exists and you are deciding whether to enforce it.

For Ledger, run all three tests and the seams turn out not to be where the nouns are. Invoices and customers are written together, change together and must be transactionally consistent — they are one module however many tables they use. Webhook delivery, by contrast, shares no transaction with anything, changes on its own schedule, and can be minutes behind: it is a genuine seam even though it is not a noun anyone would have listed.

4The modular monolith, defended

Most of what people want from microservices is module boundaries. Almost none of it requires a network.

The stated reasons for splitting are usually: independent deployment, independent scaling, technology choice per component, fault isolation, and clear ownership. It is worth checking each against a modular monolith — one deployable, with strictly enforced internal boundaries — because three of the five come for free:

What you wantedModular monolithVerdict
Clear ownershipA directory with an owners file and a public interfaceFree
Boundaries that cannot be violatedA build rule that fails when module A imports module B’s internalsFree, and stronger — it fails at compile time rather than in production
Fault isolationPartial: a bug in one module can still take the process downWeaker. This one is real
Independent scalingPartial: you scale the whole deployable, though you can run the same binary with different rolesWeaker, and cheap hardware buys a lot of headroom before it matters
Independent deploymentNoThis is the only one you cannot have. It is also the only one worth the price

The build rule in the second row is what makes this real rather than aspirational. A modular monolith where the boundaries are a convention is just a monolith with good intentions, and it decays within a year. A modular monolith where the build fails on a cross-module import of anything not in the published interface has the same enforcement a service boundary has, and it delivers the error to a developer in eight seconds instead of to a customer in production.

The practical recommendation, which is Fowler’s and has aged well: start with a monolith, modularise it as the boundaries reveal themselves, and extract a service only when you have a specific reason that a module cannot satisfy. Extracting a well-defined module later is a manageable project. Merging two services that turned out to be one thing is a much worse one, because by then two teams own them, they have separate databases, and the data has diverged.

5Conway’s law as a design constraint

“Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.” Conway wrote that in 1968 and it has never stopped being true.

The observation is usually quoted as a warning. It is more useful as a tool, because it works in both directions: if your architecture and your team structure disagree, the team structure wins, slowly, and the architecture drifts to match it. Two teams sharing one module will find a way to divide it, formally or otherwise. One team owning three services will operate them as one system and stop maintaining the contracts between them.

The practical version, sometimes called the inverse Conway manoeuvre: decide the architecture you want, then arrange the teams to match it. If you want three independently deployable services, you need three teams that can each make decisions without asking the others. If you have one team of five, you have one deployable, whatever the diagram says — and building three services with one team gives you all of the distributed-systems cost and none of the organisational benefit, because the coordination those services were meant to eliminate is happening in the same standup anyway.

Team shapeArchitecture it will produceFighting it costs
One team, 3–8 peopleOne deployable, modular if they are disciplinedVery high. Services here are pure overhead
Two teams, one productTwo deployables, or one with a contested boundaryModerate. Worth deciding deliberately which
Several teams, distinct domainsA service per team, whether or not you plan itLow — you are going with the grain
Teams organised by layer (front end, back end, data)A layered system where every feature crosses all three teamsExtremely high, and it shows up as delivery speed rather than as an architecture problem

That last row is worth dwelling on because it is common and rarely diagnosed as an architecture issue. If every feature requires three teams to coordinate, the bottleneck is the team boundary and no amount of service extraction will help — the services will be organised by layer too, and a feature will still cross all of them.

6Build: drawing Ledger’s seams

Five engineers, one product, and a real answer that is smaller than the diagram people expect.

Applying the three tests from section 3 to Ledger produces four modules and exactly one candidate for extraction:

ModuleTransaction testChange testStaleness testVerdict
Billing (invoices, customers, payments)Must be atomic togetherChanges together constantlyNo — a stale balance is wrongOne module. Not three, whatever the nouns suggest
Identity (accounts, sessions, permissions)Separate writesRarely changes with billingSeconds are fine for most of itA module with a clean interface; extractable later
Notifications (webhooks, email)No shared transactionOwn schedule entirelyMinutes are fineA genuine seam. Extract when there is a reason
AI drafting (retrieval, model calls)No shared transactionChanges on its own cadenceEntirely asynchronous alreadyA genuine seam, and the strongest candidate

The recommendation for a five-person team is a modular monolith with those four modules, enforced by a build rule, and nothing extracted yet. The two genuine seams are recorded as seams — which means the interfaces between them and the rest are kept narrow and explicit, so that extraction later is a week rather than a quarter.

The AI drafting module is the interesting one, and it is the case where extraction has an argument beyond deployment independence. Its resource profile is genuinely different: requests take seconds rather than milliseconds, they are bounded by a third party’s rate limit rather than by CPU, and one slow provider afternoon should not consume the connection pool that serves the invoice list. That is the bulkhead argument, it is a real one, and it is worth noting that it can be answered within a monolith by giving that work its own worker pool and queue — which is the cheaper version of the same isolation, and the one to try first.

Checkpoint

Playground

Your own system. Set the constraints to match your situation and see which shape the weighting actually supports — and notice how much has to change before four services wins.

Exercise set

Twelve problems: seven build the analysis tools — availability across a call graph, coupling from commit history, cycle detection, minimum cut — and three ask you to make a decision and defend it against stated constraints. Your work is saved in this browser.

All Warm-up Core Challenge Reset chapter

Chapter 14 — Choosing the Stack by Experiment

Stack arguments are unfalsifiable until somebody measures. Next chapter: success criteria before the spike, the four ways a benchmark lies, and which decisions are doors you cannot walk back through.

Continue →