Distribution Boundaries

The Distributed Monolith: All of the Cost, None of the Autonomy

Many services, tightly coupled synchronously, sharing deployment assumptions and often a database schema. You pay every distributed-systems cost — network failure, ambiguous outcomes, versioning, tracing, operational overhead — and receive none of the independence those costs were supposed to buy.

▶ Run the lab

The question this answers

The question

We have twelve services. Why does everything still have to ship together?

The guarantee — the property claimed, and its scope

None, which is the point. A distributed monolith provides neither the atomicity and compiler-checked refactoring of a real monolith nor the independent deployability of real services. Its availability is the product of all its parts, and its change velocity is that of its slowest component.

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

Each service knows only its immediate callers and callees, which is exactly why the pattern is invisible from inside. No service can see that a business change requires touching four of them, that all twelve must be deployed in a fixed order, or that its own availability is bounded by a peer three hops away. The coupling is a property of the system, and it is observable only from outside — in the change log, the deploy pipeline and the incident history.

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?
anti-patterncouplingmicroservicesdeployment

Symptoms you can check today

The diagnosis does not require an architecture review. Every symptom below is a fact recorded somewhere in your tooling, and any of them alone is a strong signal.

The most reliable is the first. A single business change requires commits in four repositories. If adding one field to a form means touching the frontend, the API service, the order service and the shared schema, the boundaries do not align with how the domain changes, and no amount of interface polish fixes that.

The second is almost as reliable: services must be deployed together, in a specific order. If the deploy runbook says "ship inventory before orders, then pricing", the independence you were buying does not exist. A real boundary means either side can ship at any time and the other keeps working.

  • One business change touches three or more repositories — the boundaries do not match the domain.
  • Deployments have a required order, or a release train that everything rides.
  • Services share a database schema, so a migration is a fleet-wide event.
  • Every request path is synchronous end to end; nothing is queued or deferred.
  • A local dev environment needs all twelve services running to do anything.
  • Integration tests require the whole system, because no service has a meaningful contract of its own.
  • One service going down takes the product down — every time, for every service.
  • Rolling back one service requires rolling back three.
  • Shared client libraries carry business logic, so upgrading a library is a coordinated release.
# release-2026-08.md  — "12 independent microservices"

1. apply schema migration 0142            (all services read this schema)
2. deploy inventory-svc  >= 4.2.0         (adds reserved_at column use)
3. deploy pricing-svc    >= 3.9.0         (must be after inventory)
4. deploy order-svc      >= 7.1.0         (breaks with pricing < 3.9.0)
5. deploy checkout-bff   >= 2.4.0
6. smoke test, then deploy the remaining 7 in any order
7. ROLLBACK: reverse steps 5->2, then migration 0142 down

# read step 4: order-svc has a *version* dependency on pricing-svc.
# That is a compile-time coupling implemented over HTTP.
The deploy runbook is the diagnosis

How systems get here — nobody chooses it

The distributed monolith is never designed. It is the default outcome of splitting along the wrong lines, and there are four common routes.

Splitting by technical layer. A "controller service", a "business logic service", a "data access service". Every request traverses all three, so every change touches all three, and the layers were never independent — they were a call stack that has been given network addresses.

Extracting without splitting the data. The code moves out; the database stays shared. Every service now reads and writes the same tables, so the schema is a global interface, and a migration must be coordinated across everything. This is [[shared-database]] as the load-bearing coupling, and it is the single most common cause.

Splitting by noun without checking behaviour. "User service", "order service", "product service" look clean on a diagram, and then it turns out every operation needs data from all three, so every request fans out and every change ripples. Boundaries must follow how the system *changes and fails*, not how the entities are named.

Chatty synchronous extraction. A loop that made ten in-process calls now makes ten network calls. The caller cannot proceed without every one of them, so the two components have the same availability, the same latency profile and the same fate — with serialisation overhead added.

Layer-split: three services, one call stack, one fate
synchronous, every requestsynchronous, every requestController svcLogic svcData-access svcShared schemaEvery change touches all three
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

Why it is strictly worse than the monolith it replaced

This is not "microservices done imperfectly". On the axes that matter it is worse than the monolith it came from, and it is worth being precise about why.

Availability multiplies down. A monolith at 99.9% is 99.9%. Twelve services at 99.9% on a synchronous critical path give roughly 98.8%, which is a tenfold increase in downtime for a system that has gained nothing. Atomicity is gone. What was one transaction is several, with visible intermediate states and no compensation designed, because nobody planned to need one. Refactoring lost its compiler. Renaming a field was a mechanical, checked operation; now it is a versioned contract migration coordinated across teams. Debugging lost the stack trace and gained a distributed trace that may or may not have been built. Latency got worse by the sum of the hops.

And the coordination cost — the thing the split was supposed to remove — is unchanged or higher, because the deploy order now has to be managed explicitly by humans rather than by a build.

The one thing that did improve is optics. It looks modern, and it maps onto team names. That is the whole benefit, and it is why the pattern survives.

MonolithDistributed monolithIndependent services
Availability of the critical pathassumptionOne component’sProduct of all of themProduct of the *critical* ones only
AtomicitytypicalTransactionalLost, uncompensatedLost, compensated by design
Refactoring across the boundarytypicalCompiler-checkedVersioned migration, coordinatedVersioned migration, independent
DeploytypicalOne artefactOrdered, all togetherAny service, any time
DebuggingtypicalStack traceDistributed trace, if builtDistributed trace, if built
Team coordinationtypicalHigh, in one codebaseHigh, across repos and pipelinesLow — the point of the exercise
Three architectures on the axes that matter

Getting out without a rewrite

The exit is not "go back to a monolith" and it is not "do microservices properly". It is to attack the specific couplings, in the order that gives the most independence per unit of work.

Split the data first. Shared schema is the coupling that makes everything else impossible to fix: while two services write the same tables, neither can migrate independently. Give each piece of state exactly one owner — this is [[data-ownership]] — and make everyone else ask or subscribe. This is the hardest step and it unblocks all the others.

Then break synchronous chains. Every hop that does not need to be on the request path becomes an event or a queued job. A dependency the caller does not wait on cannot bound the caller’s availability. Then make contracts genuinely backward compatible, so deploy order stops being a runbook step: expand, migrate, contract, never a breaking change in one release.

Then re-cut the boundaries that are still wrong, along the lines where the system actually changes. The evidence is free and already collected — look at which repositories appear together in the same change, and cut where that co-occurrence is lowest.

And consider merging. Two services that always change together and always deploy together are one component with extra latency. Merging them back is not a defeat; it removes a boundary that was buying nothing, and it is the cheapest independence you will ever gain.

Key points

  • Many services, tight synchronous coupling, shared deployment assumptions: every distributed cost, no autonomy.
  • Diagnostic symptoms are already recorded in your tooling — multi-repo changes, ordered deploys, shared schema.
  • Nobody designs it; it results from splitting by layer, by noun, or without splitting the data.
  • It is strictly worse than the monolith it replaced: availability multiplies down, atomicity is gone, refactoring lost the compiler.
  • The exit order is data first, then synchronous chains, then contracts, then re-cut boundaries.
  • Merging two services that always change together is a legitimate and cheap improvement.

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
  • A system is split along lines that do not match how it changes or how it fails.
  • Data stays shared, so schema becomes a global interface that no service can migrate alone.
  • Call paths remain fully synchronous, so every service inherits the availability of everything downstream.
  • Contracts change in a breaking way, so deploys acquire a required order.
  • The ordering hardens into a release train, and the services become one deployable unit with network hops between its parts.
What can fail at the boundary
  • Any service on the synchronous path takes the product down.
  • A schema migration breaks a service the migrating team did not know was reading those tables.
  • A deploy performed out of order produces incompatible versions in production.
  • A rollback of one service requires rolling back others, extending the incident.
  • An in-flight multi-service operation is left half-applied with no compensation, because atomicity was assumed rather than designed.
How it fails — what an operator sees
  • Total outage from one component: the operator sees the whole product unavailable because a service nobody considers critical is down, since every request traverses it synchronously.
  • Migration collateral: the operator sees a service fail immediately after an unrelated team’s schema migration, with no code change in the failing service.
  • Ordered-deploy breakage: the operator sees deserialisation or 4xx errors on a fraction of traffic mid-release, because two services are running versions that were never meant to coexist.
  • Rollback that widens the incident: the operator rolls back the service that broke and a second service starts failing, because it had already been deployed against the new contract.
  • Half-applied business operations: the operator finds orders that are paid but not reserved, with no compensation path, because what used to be one transaction is now four calls.
  • Availability arithmetic: the operator sees an overall success rate around 98.8% with every individual service reporting a healthy 99.9%, and no single service to blame.
Where coordination is required
  • Coordination is the defining cost here: the deploy order, the shared migration and the multi-repo change are all coordination that the split was supposed to remove.
  • The shared schema is the strongest coordination point — it forces every schema change to be a fleet-wide agreement.
  • Breaking that coupling requires a period of *more* coordination — dual writes, backfills, cutovers — which is why the exit is often deferred indefinitely.
  • Backward-compatible contracts are how you convert deploy-order coordination into no coordination at all; it is the highest-leverage discipline available.
What still holds under failure
  • The system provides the availability of the weakest component on the synchronous path, not the availability of any individual service.
  • Business invariants that span services are unenforced whenever any hop fails mid-operation, and there is usually no compensation because atomicity was never consciously given up.
  • Partial deploys leave incompatible versions live, so behaviour during a release window is genuinely undefined.
  • Recovery requires coordinated action across services, which is precisely what is hardest during an incident.
How it recovers
  • Detect: measure per-change repository fan-out and deploy-order dependencies as ongoing signals, not as postmortem observations.
  • Contain: for an incident in progress, roll back the whole ordered set rather than one service — partial rollback is what widens these incidents.
  • Recover: restore the failing component and re-run the ordered deploy in full, verifying version compatibility at each step.
  • Reconcile: find operations that were half-applied across services during the window and repair them explicitly; nothing else will.
  • Verify: confirm that entities touched during the incident satisfy the cross-service invariants, not merely that services are responding.
How you would know
  • Repositories per change — the single most diagnostic metric, and it is already in your version control history.
  • Whether any deploy has a required order, and how many services are in the ordered set.
  • Count of services reading or writing each database schema; anything above one is a coupling.
  • Synchronous call-graph depth on the critical path, and the implied availability product.
  • Frequency of rollbacks that required rolling back more than one service.
When it helps
  • Never as a target. Recognising it helps: naming the pattern converts a vague sense that "microservices are not working" into four specific, fixable couplings.
  • As a transitional state during a deliberate, time-boxed migration — acceptable if the coupling is being actively removed and someone owns the schedule.
When it hurts
  • Always, and increasingly with time: every new service multiplies the availability product and adds another repository to the fan-out.
  • Most acutely during incidents, when recovery needs coordinated multi-service action under time pressure.
  • For hiring and onboarding, since a new engineer must run twelve services to change one line.
Simpler alternatives
  • Merge services that always change and deploy together — the cheapest real improvement available, and it removes a boundary that was buying nothing.
  • A modular monolith with enforced internal boundaries: keeps the design discipline, deletes the network.
  • Keep the services and fix the couplings in order — data ownership, then async hops, then compatible contracts — which preserves the investment already made.
  • Split by tenant or region instead of by function: several copies of the whole system give isolation and scaling with no new interfaces at all.

Twelve services. Why does everything still have to ship together?

Twelve services. Why does everything still have to ship together?
The coupling is a property of the system, so it is invisible from inside any one service. It shows up in the change log, the deploy pipeline and the incident history.
typicalThe score is a weighted checklist, not a measurement. Its value is that every input is something you can look up today — in the deploy runbook and the last twenty pull requests — rather than something to have an opinion about.
diagnosis
A distributed monolith
signals present
6/6
score
12/12
request-path availability
99.40%
a typical change touches 4 repos
fix, in this order · Re-cut boundaries along the lines where changes actually cluster. Merge anything that always ships together — two services that always change together are one service with a network in the middle.
deploys must happen in a fixed order
fix, in this order · Make every contract change backward compatible, in both directions, for one release. Deploy order is a symptom of contracts that break; remove the breakage and the order requirement disappears on its own.
more than one service writes the same tables
fix, in this order · Give every piece of state exactly one writer. Until that is true the schema is a global interface and nobody can migrate anything without a cross-team meeting.
5 synchronous hops on the request path
fix, in this order · Move hops off the request path wherever the caller does not need the answer to reply. Each one you remove is a multiplicand deleted from the availability product and a tail deleted from the latency.
no single service can be rolled back alone
fix, in this order · Rollback-alone is the sharpest test of independence there is, because it cannot be argued with. If it fails, the boundary is decorative.
contract changes are not backward compatible
fix, in this order · Expand, migrate, contract — three deploys instead of one, and no coordination in any of them.
With 12 services in this shape you are paying every distributed cost — network failure, ambiguous outcomes, versioning, tracing, 12 on-call rotations — and receiving none of the independence those costs were supposed to buy. Availability is the product of the parts and change velocity is that of the slowest component, so it is strictly worse than the monolith it replaced: no atomicity, no compiler-checked refactoring, and no independent deployment either. The underlying error is almost always choosing boundaries by structure — splitting a call stack or an entity diagram — rather than by what changes together and what fails together.

What people believe, and what is true

Claim

We have microservices, so we get independent deployment.

Reality

Only if you can deploy any one of them at any time without breaking the others. If the runbook has an order, you have one deployable unit with network hops inside it.

Claim

The problem is that we did not split far enough.

Reality

More services multiply the coupling. The problem is the coupling — shared data, synchronous chains, breaking contracts — and each additional service makes it worse.

Claim

Sharing a database between two services is a pragmatic shortcut.

Reality

It is the coupling that makes every other coupling unfixable. While two services write the same tables, neither can migrate or deploy independently.

Claim

Merging two services back together is admitting failure.

Reality

It removes a boundary that was costing network failure, versioning and operational overhead while buying no independence. That is a straightforward improvement.

Claim

Availability is fine — every service reports 99.9%.

Reality

Twelve of those on one synchronous path multiply to roughly 98.8%, and no individual dashboard shows it. Users see the product.

Go deeper

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

Overview

Many services, tightly coupled, deployed together. You pay every distributed cost and get none of the autonomy. Check the deploy runbook and the repos-per-change count; the diagnosis is already there.

Practical

Fix in order: give every piece of state one owner so schema stops being a global interface; move hops off the request path where the caller does not need the answer; make every contract change backward compatible so deploy order disappears; then re-cut boundaries along the lines where changes actually cluster. Merge anything that always ships together.

Advanced

The underlying error is choosing boundaries by structure rather than by change and failure. A boundary is only useful where the two sides vary independently — different reasons to change, different reasons to fail, different rates of both. Splitting a call stack or an entity diagram produces boundaries with maximal coupling and no independence, and the network then makes every one of those couplings more expensive to cross than it was in memory.

Apply it

Build it, then break it
  • 🔧 Compute repos-per-change over your last fifty merged pull requests. Any cluster that always appears together is a boundary that is not paying for itself.
  • 🔧 Find every service that reads a given database schema. If the count is above one, write down what it would take to give that data a single owner.
Reason about this
  • A team proposes splitting an already-struggling twelve-service system into twenty to "finish the migration". What evidence would you gather before agreeing or refusing?
Interview questions
  • 💬 What single question would tell you whether a set of services is really independent?
  • 💬 Twelve services each at 99.9% on one synchronous path. What is the product’s availability, and what would you change first?
  • 💬 Your deploy runbook specifies an order. What does that tell you, and how do you remove it?
  • 💬 When is merging two services the right move?