Distribution Boundaries

Microservices Are a Distribution Decision, Not a Scaling Technique

Splitting a system into services does not make it faster or more scalable. It converts function calls into network calls, and in doing so hands you network failure, independent failure, distributed state, versioning, observability cost and deployment coordination — every subject in this domain, per boundary.

▶ Run the lab

The question this answers

The question

What do I actually get, and what do I actually pay, when I split a system into services?

The guarantee — the property claimed, and its scope

A service boundary guarantees exactly one thing: independent deployability of the code on either side, *if* the interface between them is stable and versioned. It does not guarantee independent failure, independent scaling, better performance or better scalability — each of those requires additional work that the boundary makes possible and does not provide.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

Before the split, a caller knew the callee’s outcome with certainty: the function returned or it threw, and both parties shared fate. After the split, the caller knows only what came back over a network within a deadline it chose. Every call now has a third outcome — unknown — and every piece of state the callee holds is knowable only through a message that was already stale on arrival. That is the entire change, and everything else follows from it.

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
microservicesboundariestrade-offsarchitecture

What the boundary actually creates

Take one function call and move the callee to another process on another machine. Enumerate what changed, because the list is longer than anyone expects and every item is a lesson in this domain.

Network calls. Latency goes from nanoseconds to milliseconds — six orders of magnitude — and the call can now be lost, delayed, reordered or duplicated. Independent failure. The callee can be down while the caller is up, which was impossible before. Ambiguous outcomes. A timeout tells you nothing about whether the work happened, so every non-idempotent operation across the boundary now needs an identity and a dedup story. Distributed state. Anything that was one transaction is now two, and atomicity across them requires a saga, a compensation and the acceptance that intermediate states are visible. Versioning. The two sides deploy separately, so at any moment two versions of the contract are live and both must work. Observability cost. A stack trace is replaced by a distributed trace, which someone must build and propagate. Deployment coordination. Changes that span the boundary need an ordered, backward-compatible rollout instead of one commit.

Every one of those is a real cost with a real name, and none of them is compensated for by the split itself. This is the honest framing: a service boundary is a purchase. You are buying organisational independence and paying in distributed-systems complexity, and the purchase is worth it exactly when the independence is worth more than the complexity.

In-process callAcross a service boundary
LatencytypicalNanosecondsMilliseconds — six orders of magnitude
OutcomesprotocolReturned or threwReturned, threw, or unknown
FailureprotocolShared fateIndependent — callee down, caller up
AtomicityprotocolOne transactionTwo, plus a compensation you must design
RefactoringtypicalCompiler-checked, one commitVersioned contract, ordered rollout, two live versions
DebuggingtypicalA stack traceA distributed trace you had to build
Argument passingprotocolA referenceA serialised copy that can be stale on arrival
One function call, before and after the boundary

The scaling claim, examined

"Microservices scale better" is the most common justification and it does not survive contact with the arithmetic. Scaling is achieved by running more instances of something stateless behind a load balancer. A monolith is stateless in exactly the same way and scales in exactly the same way — you run twenty copies of the whole thing. Splitting is not required for that, and never was.

What splitting buys is *differential* scaling: the ability to run forty instances of the image-processing service and three of the account service, rather than forty of everything. That is a genuine benefit, and it is a cost efficiency benefit rather than a scalability one. It matters when the resource profiles genuinely differ by a large factor — one component is GPU-bound, another holds a large cache, another is IO-bound and mostly idle. It does not matter much when every component is a similar web handler, which is the common case.

Meanwhile the split can make throughput *worse*. A request that was one process is now five network hops, each adding latency and each capable of failing; the tail latency of the whole is worse than the tail of any part, and serialisation between services costs real CPU that in-process calls did not. The honest summary: splitting changes the shape of your scaling costs; it does not raise your ceiling.

  • A monolith scales horizontally too — more copies behind a load balancer. Splitting is not what enables that.
  • What splitting enables is differential scaling of components with genuinely different resource profiles.
  • Fan-out makes tail latency worse: the slowest of five hops governs the response.
  • Serialisation and network I/O add CPU cost that the in-process version did not pay.
  • A shared database behind the split means you did not remove the actual scaling bottleneck.

The reasons that are actually good

The defensible reasons for a boundary are mostly organisational and risk-related rather than technical, and they are worth naming precisely so a proposal can be checked against them.

Independent deployment by independent teams. Two teams that block on each other’s release cycles have a coordination cost that grows with team count. A boundary converts that into a contract. This is the strongest reason and it presupposes that the teams already exist and are already blocking each other.

Fault isolation. A component that can consume unbounded memory or that calls an unreliable third party is better placed where its failure cannot take the rest with it. Note this requires actual isolation — separate pools, separate hosts, separate limits — not just a separate repository.

Genuinely different resource profiles, as above. Different rates of change, where a stable core does not want to be redeployed for every experiment at the edge. Different compliance or data-residency requirements, where some data must live in a particular jurisdiction or under stricter controls, and a boundary is the cleanest way to enforce that.

Every one of those is about *independence*. If a proposed boundary does not buy independence of deployment, of failure, of scaling profile, of change rate or of data governance, it is buying nothing and costing the full list from the first section.

The purchase: what you buy, and what you pay, per boundary
only if teams are blockingonly with real isolationonly if profiles differalwaysalwaysalwaysOne service boundaryBUY: independent deploymentBUY: fault isolationBUY: differential scaling & governancePAY: network calls, ambiguous outcomesPAY: distributed state, sagas, compensationPAY: versioning, observability, rollout order
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

The cost is per boundary, and it compounds

The decision is not "microservices or monolith". It is, for each proposed cut, "is the independence worth the distributed-systems cost of this specific boundary?" Some cuts pay for themselves immediately. Most do not. Framing it as an architecture-wide choice is what produces both of the bad outcomes: a hundred services nobody can operate, and a monolith with a team of forty blocking on one release train.

And the costs compound. Two services have one boundary. Ten services have up to forty-five potential interaction paths, and the ones that exist are rarely the ones on the diagram. Each boundary needs its own contract, its own versioning discipline, its own failure handling, its own trace propagation. The overhead grows faster than the service count, which is why the practical advice — start with a modular monolith and extract along a seam when a specific independence becomes valuable — is not conservatism. It is the recognition that you can add a boundary later far more cheaply than you can remove one.

Architecture owns the pattern catalogue: microservices, modular-monolith, monolith. What this domain contributes is the price tag, and the observation that the price is paid in exactly the subjects that fill the other twenty-one modules here.

Key points

  • Microservices are one architecture that creates distributed-system boundaries — not a scalability technique.
  • Every boundary adds network calls, independent failure, ambiguous outcomes, distributed state, versioning, observability cost and deployment coordination.
  • A monolith scales horizontally too; what splitting buys is differential scaling, which is a cost benefit.
  • The good reasons are independence: of deployment, of failure, of resource profile, of change rate, of data governance.
  • The decision is per boundary, not per architecture, and the cost compounds faster than the service count.
  • Adding a boundary later is far cheaper than removing one.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • Identify a seam where independence would have concrete value today, not hypothetically.
  • Name which independence the boundary buys — deployment, failure, scaling, change rate or governance.
  • Enumerate the distributed costs this specific boundary incurs: which calls become remote, which transactions split, which state is duplicated.
  • Define the contract, its versioning policy and its compatibility window before extracting anything.
  • Decide how the caller behaves when the callee is unavailable — that decision is now mandatory.
  • Extract, and verify that the two sides can genuinely deploy independently. If they cannot, you have a [[distributed-monolith]].
What can fail at the boundary
  • Every call across the boundary can be lost, delayed, reordered, duplicated or answered ambiguously.
  • The two sides can be running incompatible versions of the contract at the same time.
  • A transaction that used to be atomic now leaves visible intermediate state when the second half fails.
  • The callee can be slow rather than down, converting the caller’s latency budget into occupancy.
  • Data duplicated across the boundary drifts, and nothing reports it.
How it fails — what an operator sees
  • Latency regression after extraction: the operator sees p99 on a user-facing flow triple after a split, because a loop that made ten in-process calls now makes ten network calls.
  • Partial write visible to users: the operator sees orders in a state that was previously impossible, because the two halves of what used to be one transaction now commit separately.
  • Version mismatch in production: the operator sees deserialisation errors on a fraction of requests during a rollout, because a field was made required on one side before the other side sent it.
  • Independence that is not real: the operator sees a routine deploy of one service break another, revealing that the boundary bought no deployment independence at all.
  • Debugging regression: the operator has an error with no stack trace crossing the failing hop, because trace propagation was not part of the extraction.
Where coordination is required
  • The boundary is meant to *remove* coordination between teams; if it does not, it has failed at the only thing it reliably buys.
  • Contract changes are the residual coordination, and they must be managed by compatibility rather than by scheduling: expand, migrate, contract.
  • Any invariant spanning the boundary now requires either a saga or an accepted window of violation — see [[cross-service-transactions]] and [[protecting-invariants]].
  • Deployment order becomes a coordination artefact for changes that touch both sides, which is precisely the cost the boundary was supposed to eliminate — hence the emphasis on backward compatibility.
What still holds under failure
  • Each side retains its own local guarantees; nothing spans the boundary unless you build it.
  • The caller must define behaviour for the callee being absent, and "throw an exception" is a decision with product consequences, not a default.
  • Data replicated across the boundary is eventually consistent at best, and stale in a window nobody has measured unless somebody measured it.
  • Invariants that used to be enforced by a database constraint are now enforced by convention across two systems, which is to say not enforced.
How it recovers
  • Detect: per-boundary error rate, timeout rate and version-mismatch counters — a boundary without its own signals is invisible.
  • Contain: the caller degrades or fails fast per its declared policy, so one boundary’s failure does not become the product’s failure.
  • Recover: restore the callee, and drain any queued or retried work at a controlled rate.
  • Reconcile: compare state that is duplicated across the boundary and repair the delta, because the split created a derivation whether or not anyone called it that.
  • Verify: confirm the two sides agree on the entities affected during the window, not merely that calls are succeeding again.
How you would know
  • Per-boundary call volume, error rate, timeout rate and latency distribution, attributed to the boundary rather than to a service.
  • Contract version distribution in production — how many versions are actually live right now.
  • Count of changes requiring a coordinated deploy across the boundary; a rising count means the independence is eroding.
  • Cross-boundary data drift, measured by comparison rather than assumed absent.
  • Trace completeness across each boundary, since a boundary with broken propagation is a permanent debugging blind spot.
When it helps
  • Multiple teams whose release cycles genuinely block each other today.
  • Components with resource profiles that differ by a large factor — GPU work, large caches, long-running jobs.
  • Isolating a component that fails in ways you cannot control: an unreliable third party, an unbounded workload, untrusted code.
  • Data that must be governed differently — residency, retention, access control.
When it hurts
  • One team, one release cycle: you pay the whole distributed cost and there is no coordination to remove.
  • Boundaries drawn along technical layers rather than data ownership, which guarantee chatty synchronous traffic across them.
  • Early-stage systems whose domain boundaries are still moving — every boundary you get wrong becomes a versioned contract you must migrate.
  • Anywhere a strong invariant spans the proposed cut, since you are converting a database constraint into a saga.
Simpler alternatives
  • A modular monolith: enforce module boundaries in code, with explicit interfaces and no cross-module data access, and deploy as one unit. You get most of the design discipline and none of the network.
  • Extract only the one component that needs independence, and leave the rest — most systems need one or two boundaries, not twenty.
  • Separate the read path only, deploying a read-optimised service against a replica, which buys scaling without splitting write ownership.
  • Vertical partitioning by tenant or region rather than by function: several copies of the whole system, which scales and isolates without introducing a single new interface.

What a service boundary actually costs, per operation

What a service boundary actually costs, per operation
Splitting a system does not make it faster or more scalable. It converts function calls into network calls — and hands you this domain, once per boundary.
simplifiedLatency comes from the engine's log-normal fit through your p50 and p99. The parallel figure is exact for that model — the slowest of k calls. The sequential figure is a floor: medians add, and at least one call pays its own tail, so the real number is worse.
one call, p99
120 ms
user-visible p99 (max of k)
243 ms
operation availability
99.40%
new failure modes
6 × unknown
Where the time goes
one call · p508.0 ms
one call · p99120.0 ms
slowest of 6 · p99243.4 ms
Parallel calls cost the maximum, not the sum — and the maximum of 6 draws has a far worse tail than any one of them. A 1-in-100 slow call becomes near-certain by k = 100, which is how a per-service p99 that looks fine becomes the user's typical experience.
6 dependencies at 99.90% each multiply to 99.40% for the operation. That arithmetic is why "each service is highly available" and "the product is highly available" are different claims.
The bill, per boundary
A versioned contract
The compiler used to check the interface. Across the boundary nothing does, so versioning replaces it — and versioning is weaker.
A timeout and a deadline budget
Every call now has a third outcome — unknown — and something has to decide how long to wait for it.
Retries, and therefore idempotency
A timeout tells you nothing about whether the work happened, so any retry needs a key the callee can dedupe on.
A defined behaviour when the callee is gone
Decided in advance, at the call site, as data. At request time there is nobody to ask whether a partial answer is acceptable.
Correlated traces and logs
The request no longer exists anywhere as a whole. Reassembling it requires an identifier every fragment carried.
A saga or a reconciliation, for anything that was one transaction
Compensation is not rollback: it is a new action, it can fail, and it leaves the intermediate state visible.
Backward-compatible change, or a deploy order
If the two sides must ship together the boundary bought you nothing and cost you a network.
A service boundary guarantees exactly one thing: independent deployability of the code on either side, if the interface between them is stable and versioned. Not independent failure, not independent scaling, not better performance — each of those is additional work the boundary makes possible and does not provide. The trade you are actually making is that weaker technical guarantees buy weaker organisational coupling, which is only worth it where organisational coupling is the binding constraint.

What people believe, and what is true

Claim

Microservices scale better than monoliths.

Reality

Both scale by running more instances. Splitting buys differential scaling — running different amounts of different components — which is a cost-efficiency benefit, and it can make end-to-end latency worse.

Claim

Splitting the code makes the system more reliable.

Reality

It introduces independent failure, which is a new failure mode as much as an isolation benefit. It improves reliability only if you also build degradation, isolation and containment on each boundary.

Claim

We will split now and figure out the contracts later.

Reality

The contract is the boundary. Without a versioning policy the two sides cannot deploy independently, which removes the only guarantee the split actually offers.

Claim

A service per team is the right granularity.

Reality

Sometimes. A team may own several services or one; the question is where independence has value, and team structure is evidence about that, not the answer.

Claim

We can always merge services back if it does not work.

Reality

Merging means undoing separate datastores, separate deploy pipelines, separate contracts and the code written to tolerate their partial failure. Adding a boundary is cheap; removing one is a project.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

A service boundary turns a function call into a network call and hands you this entire domain. It buys independence and costs distributed complexity. Judge each boundary on that trade, not the architecture as a whole.

Practical

For each proposed cut, name the independence it buys and list the calls that become remote, the transactions that split and the state that duplicates. Define the contract and its versioning first. Decide the caller’s behaviour when the callee is gone. Then extract — and verify the two sides really can deploy independently.

Advanced

Read the boundary as a coordination trade. Inside a process, coordination is free: the compiler checks your interfaces and a transaction gives you atomicity. Across a boundary, both must be rebuilt in protocol — versioning replaces the compiler, sagas replace the transaction — and each replacement is weaker than what it replaced. You accept weaker technical guarantees to buy weaker *organisational* coupling. That is the whole trade, and it is only worth making where organisational coupling is the binding constraint.

Apply it

Build it, then break it
  • 🔧 Take a proposed service extraction in your system and write down every call that becomes remote and every transaction that splits. Then say what independence the split buys.
  • 🔧 Count the changes in the last quarter that required a coordinated deploy across one of your boundaries. That number is the independence you are actually getting.
Reason about this
  • A team splits an order flow into four services and end-to-end p99 goes from 300ms to 1.4s. Nothing else changed. What happened, and what would you look at first?
Interview questions
  • 💬 Someone proposes splitting a service to improve scalability. What questions do you ask?
  • 💬 List what changes about a single function call when the callee moves to another machine.
  • 💬 What does a service boundary actually guarantee you?
  • 💬 When is a modular monolith the better answer, and how would you know you had outgrown it?