The question this answers
What do I actually get, and what do I actually pay, when I split a system into services?
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.
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 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 call | Across a service boundary | |
|---|---|---|
| Latencytypical | Nanoseconds | Milliseconds — six orders of magnitude |
| Outcomesprotocol | Returned or threw | Returned, threw, or unknown |
| Failureprotocol | Shared fate | Independent — callee down, caller up |
| Atomicityprotocol | One transaction | Two, plus a compensation you must design |
| Refactoringtypical | Compiler-checked, one commit | Versioned contract, ordered rollout, two live versions |
| Debuggingtypical | A stack trace | A distributed trace you had to build |
| Argument passingprotocol | A reference | A serialised copy that can be stale on arrival |
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 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.
- • 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]].
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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.
- • 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 people believe, and what is true
Microservices scale better than monoliths.
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.
Splitting the code makes the system more reliable.
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.
We will split now and figure out the contracts later.
The contract is the boundary. Without a versioning policy the two sides cannot deploy independently, which removes the only guarantee the split actually offers.
A service per team is the right granularity.
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.
We can always merge services back if it does not work.
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
- 🔧 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.
- ⚡ 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?
- 💬 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?