Fundamentals

When Not to Distribute

Most systems that adopt distribution do not need it. The costs — partial failure, ambiguity, operational surface, debugging across boundaries — are paid on day one and every day after. The benefits are conditional on assumptions that frequently turn out to be false.

▶ Run the lab

The question this answers

The question

What is the honest case for keeping this on one machine?

The guarantee — the property claimed, and its scope

A single-process system guarantees what the machine guarantees: shared memory with a real memory model, one clock, in-process calls with two outcomes, and shared fate — if it is running, all of it is running. Those are strong guarantees, and distribution trades every one of them away.

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

In a single process, a component knows the current state of every other component, exactly and immediately. That is the property being given up. Every "just make it a service" decision converts a set of facts into a set of inferences, and the inferences are permanent.

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?
complexitytrade-offsmonolithjudgment

The costs arrive immediately; the benefits are conditional

This asymmetry is the entire argument and it deserves to be stated without hedging. On the day you split a component out, you receive: partial failure between the halves, ambiguous outcomes on every call between them, an unbounded latency tail, a new deployment unit, a new failure domain, a new set of dashboards, and a debugging story that now spans two processes. You receive all of that whether or not the split ever pays off.

What you receive *conditionally* is capacity if the work partitions, availability if the failures are independent, locality if the data can be local, isolation if there is no shared dependency. Each condition is an assumption about the future, and the industry’s record on those assumptions is not good. The most common outcome of a premature split is a system with all of the costs and a fraction of the benefit — a The Distributed Monolith: All of the Cost, None of the Autonomy, which is genuinely worse than either alternative.

The honest framing is therefore not "monolith versus microservices". It is: you are buying an option, the premium is non-refundable, and you should know what you are buying it for.

When it is paidCertainty
Partial failure between componentsprotocolDay oneCertain
Ambiguous call outcomesprotocolDay oneCertain
Cross-process debuggingtypicalFirst incidentCertain
Deployment and version coordinationtypicalEvery releaseCertain
Capacity beyond one machineassumptionWhen you exceed one machineConditional on partitionability
Survives a node failureassumptionAt the failureConditional on independence
Independent team velocityassumptionIf boundaries match change patternsConditional, and often false
The ledger, entered honestly

What a single machine can actually do

The intuition most engineers carry about single-machine limits is a decade out of date. A commodity server today has dozens of cores, hundreds of gigabytes of RAM, and NVMe storage that delivers hundreds of thousands of IOPS. A well-written service on such a machine handles tens of thousands of requests per second; a relational database on one handles workloads that most companies will never reach.

Meanwhile the constant factors of distribution work against you. A local function call is nanoseconds; a call to a service in the same zone is a few hundred microseconds at best — three to four orders of magnitude. Splitting a request path into six services turns one in-process traversal into five network hops with five latency tails and five failure domains. It is entirely normal for a decomposed system to be *slower* than the monolith it replaced, at higher cost, and for that to be discovered after the migration.

This is not an argument that single machines are always enough. It is an argument that the number where they stop being enough is much higher than the number at which teams typically split, and that the difference is usually the algorithm, the index or the N+1 rather than the machine.

The organisational reasons, priced honestly

The strongest real-world case for splitting is rarely technical. It is that two groups of people want to release independently, own their operational fate, and stop coordinating on a shared deploy. That is a legitimate benefit and it is worth money. It also has a precondition that is easy to skip: the boundary must match how the system changes. If a typical feature touches three of your services, you have not decoupled the teams; you have added a release-coordination protocol implemented in humans.

It is also worth noticing that most of the deployment-independence benefit is available without the network. A modular monolith with enforced internal boundaries — separate modules, no reaching into another module’s data, an explicit interface — gives you the design discipline, the ability to reason about ownership, and a straightforward path to extraction later, at the point where you have evidence about which boundary is real. Architecture treats the pattern; the distributed-systems point is that you can adopt the boundary without adopting partial failure, and the boundary is where most of the value is.

The reverse is not true. Once the network is in the middle, taking it out again is a migration. Boundaries are cheap to add and expensive to remove, which argues for adding them late and in the place the evidence points to.

  • Does a typical feature touch one component or several? Several means the boundary is wrong.
  • Do the two halves have genuinely different scaling or availability requirements? If not, the split buys nothing technical.
  • Can the team debug a request that crosses two processes today, with the tooling that exists today?
  • Is there an on-call rotation that can carry a second failure domain?
  • Would a module boundary inside one deployable get you most of the benefit?

The cases where the answer is genuinely "do not"

A few situations are clear enough to state as guidance rather than as trade-off. When the components must never disagree: two things that share an invariant are cheaper as one unit than as two with a consensus protocol between them, and every attempt to enforce a cross-service invariant reinvents distributed transactions badly. When the team is smaller than the number of services: you are guaranteeing that each service has less than one owner. When the requirement is unknown: you are choosing a partition of a problem you have not yet understood.

And a subtler one: when the split would make failure correlated rather than independent. Splitting a component out onto separate machines that share a database, a cache, or an identity service does not give you two failure domains. It gives you one failure domain with an extra network hop in it — the worst possible trade, since you pay the distribution cost and keep the shared fate. Fault Domains: What Fails Together and Correlated Failure: The Independence Assumption Is Usually False are where this is worked through, and it is worth checking before the split rather than during the incident.

Key points

  • Distribution costs are certain and immediate; distribution benefits are conditional and deferred.
  • A single modern machine is far larger than most teams assume, and network calls are three to four orders of magnitude slower than local ones.
  • Most of the organisational benefit of services comes from the boundary, not from the network — and a module boundary is reversible.
  • A split that leaves a shared dependency buys the cost of distribution and none of the failure independence.
  • Boundaries are cheap to add and expensive to remove; add them where evidence points, not where the diagram looks tidy.

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
  • Write down which of the four reasons in Why Distribute At All applies, as a number.
  • Check whether a bigger machine, an index, or removing an N+1 would satisfy that number.
  • If a boundary is genuinely needed, introduce it as a module inside one deployable and enforce it.
  • Observe for a period which boundaries the change patterns actually respect.
  • Extract only a boundary that has survived that observation, and only when a reason with a number requires it.
What can fail at the boundary
  • The chosen boundary turns out to be crossed by most features, so releases must be coordinated anyway.
  • The extracted service shares a database with its former host, so neither can fail independently.
  • Latency regresses because a single traversal became five hops, and the regression is discovered in production.
  • The team cannot operate the second failure domain, so incidents take longer than before the split.
How it fails — what an operator sees
  • Distributed monolith: services that must be deployed together and fail together. The operator sees a change-failure rate that rose after the split and release trains that involve three teams.
  • Latency regression after decomposition: the same user action now costs five network hops. The operator sees p99 double with every individual service reporting healthy p99s.
  • Shared-database coupling: a schema change requires simultaneous deploys of two services. The operator sees an incident caused by a rollback that could only roll back half the system.
  • Ownership vacuum: more services than engineers, so several have no active owner. The operator sees stale dependencies, unpatched images, and alerts routed to a rotation that does not know the service.
Where coordination is required
  • A single process needs no distributed coordination: the language runtime provides ordering, visibility and atomicity within the machine.
  • Every boundary introduced adds coordination that must now be explicit — deployment order, schema compatibility, contract versioning, deadline budgets.
  • The coordination does not disappear when you split; it moves from the compiler, which enforced it for free, to humans and protocols, which do not.
What still holds under failure
  • A monolith fails as a unit: unambiguous, easy to reason about, and total.
  • A distributed system fails partially: less total, more ambiguous, and much harder to reason about.
  • Which of those is preferable depends entirely on whether the partial failure leaves the system in a state anyone can act on.
How it recovers
  • Detect: measure whether the split delivered its stated reason — deploy frequency, lead time, tail latency, incident count.
  • Contain: stop extracting further services until the first extraction has demonstrably paid off.
  • Recover: re-merging services is legitimate and under-used; a boundary that is not earning its keep should be removed.
  • Reconcile: consolidate data ownership so each piece of state has exactly one service that writes it.
  • Verify: check that a typical feature now touches one service rather than several.
How you would know
  • Number of services touched by a typical change, measured from commit history. This is the single most honest signal about whether the boundaries are right.
  • Deploy coordination events — releases that required more than one service to ship together.
  • End-to-end latency for a user action compared to the sum of the services’ own reported latencies; the gap is the distribution overhead.
  • Services per engineer, and the number with no clear owner.
When it helps
  • This reasoning helps most before the first split, and again before each subsequent one.
  • It is also the right lens during a post-incident review that keeps producing action items about cross-service coordination.
When it hurts
  • It becomes an obstruction when a genuine, measured constraint exists and the analysis is used to defer an inevitable change past the point where it can be done calmly.
  • It does not apply where distribution is not a choice — a mobile client, a browser, a third-party API, or a managed database are already on the other side of a network.
Simpler alternatives
  • Modular monolith: enforced internal boundaries, one deployable, no partial failure between modules. Gets most of the design benefit, reversibly.
  • Vertical scaling plus a hot standby: covers capacity and single-machine availability with almost no protocol design.
  • Extract exactly one component — the one with a genuinely different scaling or availability profile — and leave the rest.
  • Use a managed service for the part that genuinely must be distributed, so you consume a guarantee rather than build one.

Costs certain and immediate; benefits conditional and deferred

Costs certain and immediate; benefits conditional and deferred
One user action, crossing a boundary a few times. The latency and the availability are arithmetic; the asymmetry in the ledger is the argument.
user-visible median
48 ms
one slow hop dominates
p99 of one call = 180 ms
operation availability
99.6%
unavailable minutes / month
173
single call p99180 ms
slowest of 4 parallel calls, p99314.5 ms · the tail of the fan-out is not the tail of one call
When it is paidCertainty
Partial failure between componentsprotocolDay oneCertain
Ambiguous call outcomesprotocolDay oneCertain
Cross-process debuggingtypicalFirst incidentCertain
Deployment and version coordinationtypicalEvery releaseCertain
Capacity beyond one machineassumptionWhen you exceed one machineConditional on partitionability
Survives a node failureassumptionAt the failureConditional on independence
Independent team velocityassumptionIf boundaries match change patternsConditional, and often false
What the split costs, and when each line is actually paid.
Each service here is individually 99.90% available and individually fast. The operation is 99.6% available, because availability is a property of the operation across all of its dependencies rather than of any one service. Every crossing adds a median 12 ms that a local call did not cost, and a chance of the third outcome that a local call could not produce. A module boundary inside one deployable buys most of the organisational benefit at none of this price, and it is the reversible option — moving a module boundary is a refactor; moving a service boundary is a data migration plus a contract change plus a coordinated deploy.
simplifiedAvailability multiplies only if the dependencies fail independently — they usually do not, which is a separate lesson. The fan-out tail is the domain’s log-normal latency model fitted through your p50 and p99, not a measurement of your system. The sequential figure is a sum of medians: a real chain’s tail is worse than its median suggests and better than hops × p99.

What people believe, and what is true

Claim

Monoliths do not scale.

Reality

Monoliths scale vertically and horizontally behind a load balancer, as long as they are stateless. What does not scale is a single database under a write-heavy load, and that is a separate decision.

Claim

We can split now and fix the boundaries later.

Reality

Moving a boundary after extraction is a data migration plus a contract change plus a coordinated deploy. Moving a module boundary is a refactor. Choose the reversible one first.

Claim

Microservices are required for continuous deployment.

Reality

A single deployable with good test coverage and feature flags deploys many times a day. The constraint is usually the test suite and the release process, not the topology.

Claim

This is an argument against services.

Reality

It is an argument for paying the cost when there is a reason, and for choosing the boundary from evidence. Some systems genuinely need distribution on day one.

Go deeper

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

Overview

Distribution costs arrive on day one and are certain. Its benefits are conditional and arrive later, if the assumptions hold. Do not buy the option without naming what you are buying it for.

Practical

Before splitting: name the reason as a number, check whether a bigger machine or a better query satisfies it, and count how many services a typical feature would touch. Introduce the boundary as a module first. Extract only when a boundary has proven stable and a measured constraint requires it — and be willing to merge services back.

Advanced

The deepest version of the argument is about where invariants live. Any invariant that spans two components becomes, after a split, something you can only maintain with a distributed protocol — two-phase commit, a saga with compensation, or an eventual reconciliation with a window of inconsistency you must define and defend. So the right unit of decomposition is not the noun on the diagram; it is the set of state that shares an invariant. Split along invariants and the protocols stay simple. Split along org chart or along nouns, and you will spend years reinventing transactions across a boundary that should never have existed. Name the Invariant Before You Choose the Protocol and Four Questions That Test a Proposed Boundary are the working out.

Apply it

Reason about this
  • A ten-engineer team runs fourteen services and reports that every feature takes three weeks. Diagnose, using the signals in this lesson.
  • A CTO wants to split the monolith before a funding round because "investors expect microservices". Give the honest technical answer and the honest organisational one.
Interview questions
  • 💬 Argue the case for keeping a system on one machine to a team that has already decided to split it.
  • 💬 What do you receive on day one of a service extraction, regardless of whether it pays off?
  • 💬 A team splits a service out but keeps the shared database. What have they actually bought?