The question this answers
I am about to draw a line between two parts of my system. How do I know it is in the right place?
A boundary that passes all four tests guarantees that changes on one side do not force changes on the other, and that a failure on one side leaves the other with a defined, useful behaviour. A boundary that fails any of them guarantees only added latency and a new failure mode.
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.
After the cut, each side knows only what the contract carries. Anything the other side knew implicitly — an invariant it maintained, a field it validated, an ordering it relied on — is now either written into the contract or lost. The test for a good boundary is largely a test of how much implicit shared knowledge has to cross it, because implicit knowledge crossing a boundary is coupling that no interface documents.
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.
Question 1 — does it reduce coupling?
The point of a boundary is that the two sides can change without each other. So the test is empirical rather than aesthetic: how often would a change on one side require a change on the other? Not hypothetically — look at the history. Take the last fifty changes and ask, for each, whether it would have touched one side or both.
If most changes cross the line, the line is in the wrong place. That result is not a reason to write a better interface; it is evidence that the two parts belong to the same unit of change. Interfaces cannot decouple things that vary together — they can only make the coupling more expensive to exercise.
The useful reframing is that a boundary should separate things with different reasons to change. Payment processing changes when payment providers or regulations change. Recommendations change when the model or the product experiment changes. Those are different clocks, and a boundary between them removes real coordination. A boundary between "the controller" and "the service layer" separates two halves of one clock.
1# For a proposed boundary between module A and module B:2 3changes = last_50_merged_changes()4 5both = count(c for c in changes if touches(c, A) and touches(c, B))6only = count(c for c in changes if touches(c, A) xor touches(c, B))7 8coupling_ratio = both / (both + only)9 10# ratio > 0.5 the two sides share a reason to change.11# The boundary will not reduce coordination. Do not cut here.12# ratio ~ 0.2 plausible seam; check questions 2-4.13# ratio < 0.1 strong seam — these genuinely vary independently.14 15# Note what this measures: coupling as observed, not as designed.16# It is the only one of the four questions with an objective answer,17# which is why it goes first.Question 2 — does it align with ownership and data?
A boundary should enclose the data it is responsible for. If a service cannot answer a question about its own domain without calling three others, or cannot write its own state without another service writing part of it, the boundary cuts through the middle of something.
Two failure shapes come from ignoring this. The first is the boundary that owns behaviour but not data: it must call out for every fact it needs, so it is chatty, slow and unavailable whenever its sources are. The second is data owned by nobody — two services both writing the same records, each assuming it is authoritative. That is [[data-ownership]] failing, and it produces the "which one is right?" incidents that no amount of interface design prevents.
The team dimension is part of the same test. A boundary is a contract, and contracts need an owner who can say yes or no to changes. If a boundary runs through the middle of a team’s responsibilities, or if two teams jointly own one service, the contract has no decision-maker and will erode. Team structure is not a design principle here; it is evidence about where the domain’s natural seams already are.
- Each side should own the state it is authoritative for, with exactly one writer per piece.
- A service that must call out for every fact it needs owns behaviour without data — the boundary is in the wrong place.
- Two services writing the same records means the boundary did not actually divide anything.
- Every contract needs one owner with authority to accept or refuse changes.
- A boundary running through the middle of a team’s work will be crossed constantly and informally.
Question 3 — can they fail independently?
Ask it concretely: if the other side is unavailable for an hour, what does this side do? There are only three acceptable answers. It continues fully, because it does not need the other side for this operation. It degrades in a defined way, per [[graceful-degradation]]. Or it fails, and that has been consciously accepted as the cost of the boundary.
The unacceptable answer is "it would be down, and we have not thought about that". A boundary with no defined failure behaviour has not isolated anything — it has added a new way to fail while keeping the old shared fate. Twelve of those in a row is how you get the availability arithmetic of a [[distributed-monolith]].
The stronger form of the question is about invariants. If a rule must hold across both sides — "an order is never confirmed without reserved stock" — then either the boundary needs a saga with real compensation, or the invariant needs to move entirely to one side. An invariant that spans a boundary and has no protocol is simply not enforced, and the incidents it causes will be labelled data-consistency bugs rather than boundary mistakes.
| A passing answer | What a failing answer means | |
|---|---|---|
| 1. Reduces coupling?typical | Under ~20% of changes cross the line | The two sides share a reason to change; an interface will not fix it |
| 2. Aligns with ownership and data?typical | Each side owns its state; one writer per record; one contract owner | Behaviour without data, or data with two writers — the cut is mid-entity |
| 3. Can they fail independently?assumption | Continue, degrade by design, or fail by accepted decision | Shared fate with added network failure — no isolation was bought |
| 4. Avoids constant synchronous chatter?typical | Coarse calls, or async where the answer is not needed now | The boundary is inside a loop; latency and availability are now coupled |
Question 4 — does it require constant synchronous chatter?
Count the calls a single user operation makes across the proposed line. If it is one or two coarse calls, fine. If it is ten fine-grained ones, or a call inside a loop, the boundary is cutting through a unit of work that wants to be one unit.
Chatty boundaries fail on three axes at once. Latency: each hop adds round-trip time, and a loop multiplies it. Availability: the caller cannot complete without every call succeeding, so the two sides have the same fate regardless of what the diagram says. Coupling: fine-grained calls encode the caller’s internal sequence into the callee’s interface, so the callee cannot change its model without breaking the caller.
There are two fixes, and which one applies is itself diagnostic. Make the interface coarser — one call that expresses the whole intent rather than ten that walk a structure — which usually reveals that the operation belongs to the callee, not the caller. Or make it asynchronous, when the caller does not actually need the answer to proceed; that removes the availability coupling entirely, since a dependency you do not wait on cannot bound you. If neither fix is possible, the boundary is in the wrong place, and the honest response is to move it rather than to optimise the calls across it.
# chatty: the boundary sits inside the caller's loop
order-svc -> inventory-svc GET /items/{id} x 12
order-svc -> inventory-svc POST /items/{id}/reserve x 12
24 round trips; fails if any one fails; p99 = 24 x hop latency
inventory-svc cannot change its item model without breaking order-svc
# coarse: the boundary expresses one intent
order-svc -> inventory-svc POST /reservations {order_id, items[12]}
1 round trip; inventory-svc owns the loop, the transaction and the
partial-failure semantics; its item model is now private again.
# async: the caller does not need the answer now
order-svc -> queue OrderPlaced {order_id, items[12]}
0 round trips on the request path; inventory-svc can be down for an
hour without order-svc noticing. Costs: eventual consistency, and a
reconciliation path for reservations that never complete.Using the answers
The four questions are not a scorecard to average. Question 1 is close to necessary: a boundary that does not reduce coupling has no upside to weigh against its costs, whatever the other answers say. Question 3 is close to sufficient in the negative: if the two sides cannot fail independently and you have not decided what happens when one does, the boundary is adding failure modes without isolating anything.
Questions 2 and 4 are usually fixable without moving the line — data can be moved to give it a single owner, interfaces can be made coarser, calls can be made asynchronous. Question 1 is usually not fixable, because it is a fact about the domain rather than about the design.
Applied honestly, the four questions reject most proposed boundaries, and that is the correct outcome. Most systems need a small number of well-placed cuts. Architecture owns the catalogue of patterns you might choose; these questions are how you decide whether this particular cut, in this particular place, is one of the few that pays.
Key points
- Four tests: reduces coupling, aligns with ownership and data, fails independently, avoids synchronous chatter.
- Question 1 has an objective answer in your change history — measure it rather than debating it.
- A boundary should separate things with different reasons to change, not different technical layers.
- Each side must own its state, with exactly one writer per record, and one owner for the contract.
- "What happens when the other side is down for an hour?" has only three acceptable answers.
- Chatty boundaries couple latency, availability and internal models all at once; make them coarser or asynchronous, or move them.
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.
- • Propose the line, and identify precisely which state and which behaviour fall on each side.
- • Measure historical coupling: what fraction of recent changes would have crossed it.
- • Check that each side owns the data it is authoritative for, with one writer per record.
- • Write down what each side does when the other is unavailable for an hour.
- • Count the calls one user operation makes across the line, and whether any sit inside a loop.
- • Reject, move, or accept the boundary — and if accepted, write the contract and its versioning policy before extracting.
- • The boundary is placed where the two sides share a reason to change, so every feature becomes a coordinated change.
- • Data ends up with two writers, and the two sides diverge with no defined authority.
- • Neither side has a defined behaviour for the other being unavailable, so both are down together.
- • Fine-grained calls encode the caller’s internal sequence into the interface, freezing the callee’s model.
- • An invariant spans the boundary with no saga and no compensation, so it silently stops being enforced.
- • Coordinated-change treadmill: the operator — here, the team — sees every feature require pull requests on both sides of a boundary that was introduced to decouple them.
- • Divergent records: the operator sees the same entity with different values in two services and no defined answer to which is correct, because both sides write it.
- • Simultaneous outage: the operator sees both services fail together every time either one does, because neither has a defined degraded behaviour.
- • Interface ossification: the operator sees a refactor blocked because the callee’s internal model is exposed through a dozen fine-grained endpoints that consumers depend on.
- • Silent invariant loss: the operator sees confirmed orders with no stock reservation, and finds no error anywhere, because the rule that used to be a database constraint now spans a boundary with no protocol.
- • The whole purpose is to reduce coordination between teams; question 1 measures whether it will.
- • The residual coordination is the contract, and it must be managed by compatibility rather than by scheduling.
- • Question 3 exposes the coordination that failure requires: a boundary with no defined failure behaviour needs humans to coordinate during every incident.
- • Invariants that span the boundary are the most expensive coordination of all — a saga plus compensation — which is a strong argument for placing the boundary so that invariants fall entirely on one side.
- • A well-placed boundary lets each side keep its own guarantees when the other is gone.
- • A boundary failing question 3 provides the availability of the weaker side to both.
- • Invariants spanning the boundary hold only as strongly as the saga protecting them, which is to say eventually and with a visible window of violation.
- • Data with two writers has no meaningful guarantee at all during a partition; both sides accept writes and both believe they are right.
- • Detect: track the fraction of changes crossing each boundary as a standing metric — rising coupling is visible long before it becomes painful.
- • Contain: for a boundary already in place and failing question 3, add the degraded behaviour first; it is the cheapest independence available.
- • Recover: for a boundary failing question 2, move the data so each record has one writer, then make the other side a reader.
- • Reconcile: where two writers have already diverged, pick an authority, compare, and repair — see
[[reconciliation]]. - • Verify: re-measure coupling after the change; if it has not fallen, the line is still in the wrong place.
- • Fraction of changes that touch both sides of each boundary, tracked over time.
- • Calls per user operation across each boundary, and whether any occur inside a loop.
- • Number of writers per record or per table — anything above one is a question-2 failure.
- • Whether a documented degraded behaviour exists for each dependency, and whether it has ever executed.
- • Contract version churn: frequent breaking changes indicate the boundary is cutting through an unstable part of the domain.
- • Before any extraction, where the questions are cheap and rejecting a bad cut costs nothing.
- • When re-cutting an existing set of services that is not delivering independence, since the questions identify which boundaries to remove and which to move.
- • When a team wants to split for organisational reasons: the questions turn "we want our own service" into a checkable claim.
- • As a bureaucratic gate on a small system where the boundary is obvious and the cost of getting it wrong is a day’s work.
- • When the answers are asserted rather than measured — question 1 in particular is worthless as an opinion and decisive as a number.
- • When used to justify a boundary already built, at which point the exercise is rationalisation.
- • Domain-driven design’s bounded contexts, which reach similar conclusions from language and model boundaries rather than from change history — a good cross-check when the change data is thin.
- • Module boundaries inside one deployable: apply the same four questions, get the design discipline, pay none of the distributed cost, and extract later along a line already proven.
- • Deriving seams from the data model — find the entity clusters with few cross-cluster transactions and cut there; useful when behaviour is spread but data is clean.
- • Measure and wait: for a young domain, let the change history accumulate and cut when the seam is visible in the data rather than guessed.
Four questions, asked before the cut
What people believe, and what is true
A good interface makes the coupling manageable.
An interface cannot decouple parts that share a reason to change. It makes the coupling more expensive to exercise, which is the opposite of the goal.
Boundaries should follow the entities in the domain model.
They should follow reasons to change and reasons to fail. Entity-shaped boundaries routinely require every operation to touch three services.
One service per team is the right rule.
Team structure is evidence about where independence has value, not the answer. A team may own several boundaries or none.
We can make the chatty boundary fast with caching and batching.
That treats the symptom. Fine-grained cross-boundary calls also freeze the callee’s internal model and couple availability; the fix is a coarser interface, an async hop, or a different line.
The invariant will be fine — we will validate it on both sides.
Validation on both sides is not enforcement. Without a saga and a compensation, an invariant spanning a boundary is unenforced during exactly the failures that matter.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Four questions: does it reduce coupling, does it align with ownership and data, can the sides fail independently, does it require constant synchronous chatter. Ask before cutting.
Practical
Measure question 1 from your change history rather than debating it. Verify one writer per record and one owner per contract. Write down what each side does when the other is down for an hour. Count cross-boundary calls per operation and fix chattiness by coarsening or going async — or move the line.
Advanced
All four questions are probes of one property: whether the two sides vary independently in change, in failure and in load. That is what a boundary can exploit, and it cannot be created by an interface — only discovered. Which is why boundary design is mostly measurement rather than modelling, and why the best time to cut is later than teams want, when the independence is visible in the data.
Apply it
- 🔧 Pick an existing boundary in your system and compute the fraction of the last fifty changes that crossed it. Decide whether to keep, move or remove it.
- 🔧 For each of your service’s dependencies, write one sentence describing what happens if it is unavailable for an hour. Note which ones you cannot answer.
- ⚡ Two teams jointly own one service and disagree about an interface change. Which of the four questions was violated when the boundary was drawn, and what would fix it?
- 💬 How would you decide, with evidence rather than opinion, whether a proposed service boundary is in the right place?
- 💬 Your proposed boundary means one operation makes twelve calls across it. What are your options?
- 💬 An invariant spans a boundary you are about to create. What has to happen?
- 💬 Which of the four questions is hardest to fix after the fact, and why?