Fundamentals

Where the Boundary Goes

A boundary is not a line on a diagram. It is the place where shared memory ends, where partial failure enters, and where an invariant stops being enforceable by the compiler. Choose it by asking which state must never disagree — not by which nouns look separate.

▶ Run the lab

The question this answers

The question

If I am going to split this, where exactly should the cut go?

The guarantee — the property claimed, and its scope

Within a boundary, the usual single-process guarantees hold: atomic multi-step updates, immediate visibility, unambiguous call outcomes. Across a boundary, none of them do, and every guarantee you want must be reconstructed from messages. The boundary is precisely the line where that switch happens.

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

A component knows the state it owns, exactly and now. It knows about state on the other side of a boundary only as a message: a dated copy, an event it consumed, or an answer to a question it asked. A design is sound when every decision a component makes depends only on what it owns plus what it can safely infer.

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?
boundariesdecompositionownershipinvariants

A boundary is a failure surface, not an interface

The usual way boundaries get chosen is by nouns: Orders, Users, Payments, Notifications. That produces a diagram that reads well and says nothing about failure. The question a distributed-systems reading asks instead is: what happens to correctness when the two sides cannot talk? If the answer is "nothing important, one side degrades", the boundary is well placed. If the answer is "an invariant is violated and someone has to reconcile it by hand", the boundary is cutting through something that should not be cut.

Concretely: an order and its line items share an invariant — the total must equal the sum of the items. Put a network between them and you have chosen to enforce that invariant with a protocol, forever. An order and the email confirming it share nothing of the kind: if the notification service is down, the order is still correct and the email arrives later. The first cut is expensive, the second is nearly free, and no amount of diagram-tidiness changes that.

The productive reframing is that a boundary is where you agree to accept partial failure. You should therefore put it where partial failure is cheap, which is exactly where the two sides do not share an invariant.

Good signBad sign
Shared invariantassumptionNone spans the cutA rule must hold across both sides at all times
When the link breaksassumptionOne side degrades gracefullyCorrectness is violated or work halts entirely
Typical changetypicalTouches one sideRequires a coordinated release of both
Data ownershiptypicalEach side writes only its own stateBoth write the same tables
ChattinesstypicalOne call per user actionA call per item in a loop
RequirementsassumptionDifferent scaling or availability needsIdentical profile — the split buys nothing
Testing a proposed boundary

Boundaries you did not choose

It is worth noticing how many boundaries already exist in a system nobody would describe as distributed. The browser and the server. The application and the database. The application and the cache. The application and the object store, the payment provider, the identity provider, the email gateway. Each of those is a machine boundary with independent failure and ambiguous call outcomes, and each of them is a place the reasoning in this domain already applies.

This matters because it changes where the effort goes. A team debating whether to extract a service often has ten existing boundaries handled carelessly — no deadline on the payment call, no idempotency on the webhook, an ORM that lazy-loads across the database boundary in a loop. Tightening those is cheaper and higher-value than adding an eleventh.

It also gives you a source of evidence. The boundaries you already have will tell you which failure modes your system actually experiences and whether your team can operate them. If the existing database boundary produces regular incidents nobody can diagnose, adding service boundaries will not go well.

Chattiness is a property of the boundary, not the code

A boundary placed across a tight interaction produces a chatty interface, and no amount of client-side cleverness fixes it. If rendering one page requires forty facts that live on the other side, you will either make forty calls, make one call that returns everything (and couples the two sides’ schemas), or cache aggressively (and inherit staleness). All three are worse than not having cut there.

The tell during design is a proposed interface with many small operations, or one where the caller needs to loop. The tell in production is a request count on the dependency proportional to result-set size, and a p99 that grows with page size. API Design owns the granularity discussion at the contract level; the distributed-systems point is that a chatty boundary multiplies not only latency but *failure probability* — forty calls at 99.9% each is a 96% chance the page renders completely.

The corollary is a useful design heuristic: the boundary should be where the data naturally aggregates. If one side can answer a question completely, the interface is one call. If it can only answer fragments, the aggregation belongs on its side of the line, or the line is in the wrong place.

1# Boundary cuts through the aggregation: 40 crossings
2for item in cart.items: # 40 items
3 p = catalog.get_price(item.sku) # 40 remote calls
4 total += p
5# P(all succeed) at 99.9% per call = 0.999^40 = 96.1%
6# One page in 25 fails, and each failure is ambiguous.
7
8# Boundary placed where the data aggregates: 1 crossing
9prices = catalog.get_prices([i.sku for i in cart.items])
10total = sum(prices[i.sku] for i in cart.items)
11# P(success) = 99.9%. One ambiguity to reason about, not 40.
12# Cost: a coarser partial-failure story — see the API Design
13# treatment of batch endpoints for how to report per-item results.
The same failure probability, two boundary placements

Ownership is the durable form of the boundary

The version of a boundary that survives contact with a growing system is data ownership: exactly one component writes each piece of state, and everyone else reads a copy or asks. This is stronger and more useful than "each service has its own database", because it is checkable and because it is what actually prevents the failure modes — two writers producing conflicts, a schema change that breaks an unknown consumer, an invariant enforced in two places and drifting.

Once ownership is fixed, most of the other questions answer themselves. Who resolves a conflict? The owner. What does a reader do with a stale value? Ask the owner, or make the write conditional. Who can change the schema? The owner, subject to a contract with the readers. What happens during a partition? Readers degrade; the owner keeps working.

The failure to establish ownership is the root of most of the pain in The Shared Database: An Honest Trade, Not a Prohibition and The Distributed Monolith: All of the Cost, None of the Autonomy, and the reason "each service owns its data" is repeated so often. Stated as a boundary rule it is sharper: a boundary is only real if no invariant and no write path crosses it.

Key points

  • A boundary is where shared memory ends and partial failure begins — not a line on a diagram.
  • Cut where no invariant spans the cut; anywhere else you are choosing to enforce it with a protocol forever.
  • Most systems already have many boundaries handled carelessly; fix those before adding one.
  • A chatty boundary multiplies latency and failure probability; place the cut where data aggregates.
  • The durable form of a boundary is single-writer ownership of state.

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
  • List the invariants the system must maintain, and the state each one touches.
  • Group state so that every invariant lives entirely inside one group.
  • Propose boundaries between groups and test each: what breaks when the link is down, and how many crossings does a typical operation make?
  • Assign exactly one writer to each piece of state.
  • Define what each side does when the other is unreachable — degrade, queue, or refuse — before shipping.
What can fail at the boundary
  • A latent invariant spans the cut and is discovered only when the two sides disagree in production.
  • A read path becomes a loop of remote calls as features accrete, turning a fine boundary into a chatty one.
  • A second writer appears — a migration script, an admin tool, a batch job — and silently breaks ownership.
  • Schema evolution on the owner’s side breaks a consumer nobody knew existed.
How it fails — what an operator sees
  • Invariant violation across the cut: totals stop matching their components. The operator sees a reconciliation report with a nonzero delta and no errors in either service.
  • Chatty boundary: page latency grows with result-set size. The operator sees a request count on the dependency that tracks items per page, and a comb-shaped trace.
  • Second writer: a nightly job updates state owned by a service, bypassing its validation. The operator sees invalid states that the owning service could not have produced, appearing at the same time each night.
  • Coupled release: a change requires both sides to deploy in a specific order. The operator sees a failed rollback that could only revert half the system, and a maintenance window where there should not have been one.
Where coordination is required
  • Inside a boundary, coordination is free: the runtime and the database provide atomicity and visibility.
  • Across a boundary, every guarantee costs messages. An invariant spanning the cut costs either a distributed transaction, a saga with compensation, or an accepted window of inconsistency.
  • The cheapest boundaries are those across which only *notifications* flow — one side tells the other something happened, and nothing depends on the message arriving promptly.
What still holds under failure
  • Each side remains internally consistent and continues to serve what it owns.
  • Cross-boundary reads return stale data or fail; cross-boundary writes become ambiguous.
  • Invariants entirely within one side continue to hold; invariants spanning the cut do not.
How it recovers
  • Detect: run a reconciliation that compares the two sides’ views of any shared concept — the delta is the boundary’s failure surface made visible.
  • Contain: define degraded behaviour per boundary in advance, so an unreachable dependency has a designed response rather than a stack trace.
  • Recover: replay the notifications that were missed, which requires them to be durable and identified.
  • Reconcile: apply a documented resolution rule for divergence; decide it before the incident.
  • Verify: check the invariant, not the transport. Replication catching up is not the same as the totals matching.
How you would know
  • Crossings per user-facing operation, per boundary — the honest measure of chattiness.
  • Reconciliation delta between the two sides for any shared concept, as a first-class metric.
  • Count of distinct writers per table or per aggregate; anything above one is an ownership violation.
  • Number of releases requiring coordinated deploys across the boundary.
When it helps
  • Before any decomposition, and during any incident review whose action items are about cross-service coordination.
  • When onboarding a new consumer of existing data — the ownership question is the one that prevents the next class of bug.
When it hurts
  • Formal boundary analysis on a system that will be rewritten within the year is effort spent on a structure that will not survive.
  • Over-strict ownership rules can push teams into building read APIs for data that a read replica would have served fine.
Simpler alternatives
  • Do not cut: keep both sides in one deployable with a module boundary, which gives ownership discipline without partial failure.
  • Cut asynchronously: put a durable log between the sides so the boundary tolerates one side being down without the other blocking.
  • Cut for reads only: let a second component read a replica of the owner’s data while all writes still go to the owner.
  • Move the operation instead of the data: send a command to the owner rather than pulling state out to decide remotely.

Where the cut goes, and what the cut costs

Where the cut goes, and what the cut costs
Move components across the boundary. An invariant that spans the cut is a rule you have chosen to enforce with a protocol, forever.
Preset cuts
invariants spanning the cut
0
crossings per order
1
boundary
two failure domains
when the link is down
one side degrades
inside one sideorder.total equals the sum of its itemsorders:A order_items:A
inside one sideno order ships without an authorised paymentorders:A payments:A
inside one sidestock on hand never goes negativeinventory:A
inside one sideevery captured payment appears exactly once in the ledgerpayments:A ledger:A
The cut, drawn as what it is: a failure surface. Every line crossing it is a message that can be lost, delayed, reordered or duplicated.simplified
orders ↔ order_items: okorders ↔ payments: okpayments ↔ ledger: okorders · leader · uporders★ leaderorder_items · leader · uporder_items★ leaderinventory · leader · upinventory★ leaderpayments · leader · uppayments★ leaderledger · leader · upledger★ leadernotifications · follower · up — other side of the cutnotifications· follower
ok
  • notifications — other side of the cut
No invariant spans this cut, so each side can enforce its own rules locally. One order crosses it rarely, which is what a well-placed boundary looks like. The durable form of a boundary is single-writer ownership: separate storage is a consequence of that rule, not the rule itself.
simplifiedA four-invariant model of one ordering system. The test it applies — does any invariant span the cut, how many crossings does one user action make, what still works when the link is down — is the real one, and it is the one a service diagram cannot answer.

What people believe, and what is true

Claim

Boundaries should follow the domain model.

Reality

They should follow invariants and change patterns. A domain model is a good starting hypothesis and a poor final answer, because it says nothing about failure.

Claim

Each service having its own database is what makes the boundary real.

Reality

Separate storage is a consequence. The rule that makes it real is single-writer ownership; two services writing the same store have one boundary in name only.

Claim

A chatty interface can be fixed with caching.

Reality

Caching trades chattiness for staleness, which is fine for display data and not for anything guarding a write. The boundary placement is still wrong.

Claim

If the interface is well designed, the boundary is fine.

Reality

Interface quality does not affect what happens when the link is down. A beautiful interface across an invariant is still an invariant enforced by a protocol.

Go deeper

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

Overview

Put the boundary where no invariant crosses it and where one call answers a whole question. Then give every piece of state exactly one writer.

Practical

For each proposed cut, answer four questions in writing: what invariant spans it, what each side does when the link is down, how many crossings a typical operation makes, and who writes each piece of state. If any answer is uncomfortable, move the line rather than building a protocol.

Advanced

The formal version is about where you are willing to give up serialisability. Inside a boundary you have a transaction manager and can express any invariant as a constraint. Across it you have, at best, a saga: a sequence of local transactions with compensations, which is not atomic and admits intermediate states that are externally visible. So a boundary decision is really a decision about which intermediate states the business can tolerate being seen. Systems that get this right enumerate those states as part of the design — an order that is paid but not yet reserved is a real state with a real user experience — rather than treating them as transient anomalies to be hidden.

Apply it

Build it, then break it
  • 🔧 Take an existing service pair and write down every invariant that spans the boundary. For each, name the protocol currently enforcing it — including "nothing, we reconcile by hand".
Interview questions
  • 💬 How would you test whether a proposed service boundary is in the right place?
  • 💬 Why is "each service owns its data" a stronger rule than "each service has its own database"?
  • 💬 A page needs forty facts from another service. What are your options, and what does each cost?