THIS EXPLANATION
THE ROOM
CDA·209 Computing, Data & AI 7 MIN · 8 STATIONS

Splitting a system into services

A Socratic walk-through of splitting a system into services — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why does breaking one program into thirty make it slower, more fragile, and easier to run?

Take a program that works and cut it into thirty programs that talk over a network. Every function call that crossed a cut is now a request: serialised, sent, queued, deserialised, and possibly lost. A transaction that used to commit or roll back as a unit now spans several databases and cannot. Debugging one user's problem means correlating logs across a dozen processes.

By every technical measure that was easy to state, the system got worse. And yet organisations that make this change often report that shipping got easier — not that they merely believe it did, but that release frequency rose. So what exactly improved, given that nothing on the list above did?

b

Reasoning it through

REASONING #

The first thing to notice is that the question contains a category error, and finding it is most of the work. Latency, consistency, and observability are properties of the running system. Ease of shipping is a property of the organisation changing it. Nothing requires those two to move in the same direction, so a change that degrades one and improves the other is not a paradox — it is a trade with two different currencies.

So look at what actually limits a large team's throughput on a single deployable. Not typing speed — coordination. Every change enters the same build, test suite, and release, so fifteen teams sharing one pipeline means a release goes out when the last team is ready and no team's change has broken the suite. A bad commit blocks everybody, merge conflicts arrive in proportion to how many people touch the same tree, and a release train is a queue whose wait grows faster than the number of teams, since each addition brings both its own work and its chance of blocking everyone else's.

Now ask what a service boundary really is, stripped of the technology. It is a promise: this code can be built, tested, and released without asking anyone else. Everything else — the containers, the HTTP, the separate database — is machinery for making that promise enforceable. That is why the definition that survives scrutiny is not "small" but independently deployable. A team that can deploy on Tuesday without a meeting has bought back the coordination cost, and it is that, not the process boundary, that shows up as release frequency.

But independence must be paid for, and the price has a specific shape. Two modules in one process could share a type; two services need a wire contract evolved compatibly, which means additive changes and a period where both shapes are accepted. Two modules could share a transaction; two services fall back to sagas with compensating actions, or accept being briefly inconsistent. Two modules could share memory; two services must accept that a call can fail in a third way — not success, not failure, but no answer — which is why timeouts, retries, idempotency keys, and circuit breakers stop being optional. None of this is incidental complexity better tooling removes. It is the cost of the independence, and if you are not using the independence you have bought the cost for nothing.

Which yields the sharpest test available. If two services must be released together to work, you have paid every distributed-systems cost and kept the coordination you were trying to escape. That is a distributed monolith, and it is the standard failure — usually because the boundaries were drawn along technical layers rather than along things that change together.

So what makes a boundary good? The honest answer is that we have heuristics rather than a method. Draw around a business capability whose vocabulary is self-contained, so most changes land inside one boundary. Draw so a boundary maps to a team that can own it, since Conway's Law says the architecture drifts toward the communication structure anyway. And prefer to draw late: boundaries in a single codebase are cheap to move, boundaries across a network are not, so a modular monolith first, split where the seams have proved stable, is the lower-variance path.

Scale is often listed as the motivation and is usually the weakest part of it. Services let you scale a hot component alone rather than replicating everything, which is real — but replicating a whole monolith behind a load balancer is also cheap, and most systems never need more. The scaling that justifies a split is more often organisational than computational.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of a restaurant kitchen. One cook doing everything wastes no motion: nothing is handed over, nothing waits, nothing is misheard. Put fifteen cooks in that arrangement and they spend the night reaching past each other for the same pan. Split the line into stations — grill, sauce, garde manger — and you have introduced real waste: plates now travel, tickets are called across the room, and a dish that touches three stations can arrive with its parts out of sync. But each station now works without waiting for permission, and the kitchen's output rises even though every individual dish takes a slightly longer and more error-prone path.

WHERE IT BREAKS DOWN

the stations were laid out to match how dishes are actually composed, and a kitchen arranged instead by tool type — everyone who uses a knife at one bench, everyone who uses heat at another — would have all the handover cost and none of the independence, which is exactly the mistake that produces a distributed monolith and is much easier to make in software than in a kitchen.

d

Clarifying the model

THE MODEL #

The misconception to disarm is that "microservices" is a target state and a monolith is a stage you grow out of. A well-modularised monolith is a legitimate destination, and for most systems it is the right one, because the coordination cost that services buy back is only large when the team is large. The variable that matters is not the size of the codebase but the number of people who must agree before something ships.

Two refinements. First, "slower" needs qualifying: a single request crossing services is unambiguously slower than an in-process call — roughly microseconds becoming milliseconds — but total throughput can rise, because independent components scale and fail separately. Slower per request, not necessarily overall.

Second, "more fragile" deserves the same care. Failure modes multiply, but their blast radius can shrink: a memory leak in one service does not take down the process serving everything else, whereas in a monolith it does. A distributed system trades a small number of total failures for a large number of partial ones, and whether that is more or less fragile depends entirely on whether you built for partial failure. Split without circuit breakers, timeouts, and idempotent handlers and you get the multiplication with none of the containment.

e

A picture of it

THE PICTURE #
Splitting a system into services
Splitting a system into services Read left to right as coordination pressure and bottom to top as the price a network boundary would charge. The bottom-right quadrant is where splitting is most clearly worth it -- many people blocked on one release, little transactional chatter to break. Top-left is where it is least worth it. The top-right point is the trap: high coordination and high boundary cost means a split that was performed without moving leftward, so both bills arrive and nothing was bought. {"generator":"mermaid-svg-renderer@3.2.1","source":"../Socrates/.diagram-cache/_src/splitting-a-system-into-services.md","sourceIndex":1,"sourceLine":4,"sourceHash":"aa0600a32623708a0cc63ef5e75cbafe47b2d74abf69a1b47edced54aa1ea767","diagramType":"quadrantChart","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":621},"qa":{"passed":true,"findings":[]}} Split, and pay for it properly Q1 Keep the modular monolith Q2 Split freely, cheapest case Q3 Split here first Q4 Distributed monolith Batch report generator Fifteen teams, one release train Chatty transactional core Small team, one codebase Few people must agree to ship Many people must agree to ship Low cost of a network boundary High cost of a network boundary Where a split pays and where it costs

How to readRead left to right as coordination pressure and bottom to top as the price a network boundary would charge. The bottom-right quadrant is where splitting is most clearly worth it — many people blocked on one release, little transactional chatter to break. Top-left is where it is least worth it. The top-right point is the trap: high coordination and high boundary cost means a split that was performed without moving leftward, so both bills arrive and nothing was bought.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Splitting a system trades runtime properties for organisational ones, and the exchange rate depends almost entirely on how many people must agree before a change ships. The technical costs are real and unavoidable, so they are only worth paying when the independence they buy is actually used — which is why the test of a boundary is whether either side can be released alone.

g

Where to go next

ONWARD #
  • How a saga's compensating actions differ from a rollback, and what becomes impossible when compensation is not available.
h

Key terms

TERMS #
TermWhat it means
Independently deployablethe property that a component can be built, tested, and released without coordinating with another team; the working definition of a service boundary.
Distributed monolithservices that must be released together, incurring every distributed cost while keeping the coordination they were meant to remove.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4