Consistency Boundaries
Which set of things must change together, atomically, for an invariant to hold. Answer that and you have chosen your aggregates, your transactions and — later — where a service could ever be split.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Which pieces of state have to move as one, and what happens to the ones I decide do not?
"An order's total must always equal the sum of its lines, and stock must never be promised twice." Two invariants that look similar and demand completely different boundaries — which is the whole lesson.
Put both in one transaction. They are both invariants, transactions are what invariants use, and the database will sort it out.
It works, until the transaction spans the payment provider call and holds an inventory row lock for 900ms, at which point a popular product serialises every checkout in the system behind one row (Backend Engineering names this anti-pattern specifically).
- It works, until the transaction spans the payment provider call and holds an inventory row lock for 900ms, at which point a popular product serialises every checkout in the system behind one row (Backend Engineering names this anti-pattern specifically).
- The transaction grows. Once it is the place invariants go, the next feature adds loyalty points to it, then an audit row, then a notification, and the boundary becomes "everything touched by checkout" — which is not a boundary (God Object).
- It also silently decides the architecture. Two pieces of state in one transaction must be in one database, so the warehouse team cannot own inventory next year without a redesign that nobody has budgeted (The Modular Monolith).
- And it conflates two genuinely different invariants. "Total equals the sum of lines" is a claim about one thing's internal coherence and is nonsensical to observe halfway through. "Stock is never promised twice" is a claim about a scarce shared resource and has a perfectly good weaker form — reserve, then confirm — that the strong version hides.
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Orders and inventory are in the same Postgres database today, but inventory is the part a warehouse team wants to own separately within a year.
- Checkout has a 400ms budget and holding a row lock on a popular product across a payment call is not survivable.
- A promotion can put ten thousand people on the same product in a minute, so contention on one row is a real operating condition (Observability & Performance has the measurement side).
- An order's total equals the sum of its lines, at every moment any reader can observe both.
- The quantity promised for a product never exceeds the quantity available.
- Whatever is decided to be eventually consistent has a stated window and a stated remediation, rather than being eventually consistent by accident.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Each consistency boundary has exactly one owner that may modify anything inside it, which is what makes the atomicity claim meaningful (State Ownership).
- Anything crossing a boundary owns the compensation: what happens when the second half does not complete (Partial Failure).
- Somebody owns each stated window — "a reservation not confirmed within fifteen minutes is released" — including the job that enforces it and the alert when it does not run.
- The boundary is drawn by the invariant, and the test is direct: can a reader observe these two facts and see them disagree? If a disagreement is observable and unacceptable, they are inside one boundary (Invariants).
- The order and its lines are one boundary because the total is meaningless without them — this is the classic aggregate, and it is an invariant argument rather than a modelling preference (Aggregates).
- The order and the inventory are two boundaries, related by a reservation. That reservation is a first-class piece of state, not a side effect, and making it explicit is the design move (Explicit State).
- Boundaries chosen this way survive a later service split, because the thing that makes a split hard is exactly an invariant spanning it (Architecture Boundaries).
One question, two answers
The two invariants in the requirement look like the same kind of statement and are not, and the difference is entirely in whether a disagreement is observable and unacceptable.
An order whose total does not match its lines is nonsense — there is no reading of it that is merely stale. Stock that says 5 when 5 are reserved and one reservation is 200ms old is not nonsense; it is a number with a known freshness, and every warehouse in the world already works that way.
- Inside the order boundary: total and lines. Observably inconsistent would be nonsense, so one transaction (Aggregates).
- Across the boundary: a reservation, which is a real row with an owner and an expiry — not a gap between two writes.
- The window is stated: fifteen minutes, chosen because it is longer than the slowest payment and shorter than a customer's patience.
- The compensation is designed, not improvised: an unconfirmed reservation is released, and that job is monitored because its silent failure is the interesting one.
- The reconciliation exists because prevention is not total: promised versus available, every five minutes, with an alert that goes to a person (Designing for Failure).
- And the split is now possible: inventory can move to another team without the invariant following it, because it never spanned the line (The Modular Monolith).
The intermediate state, made explicit
The single most valuable thing a consistency boundary decision produces is a name for the in-between. Without reserved, a crash between the two writes leaves state that no part of the system has a word for, and the recovery code has to infer intent from timestamps.
The forbidden transitions below are the design. Each one corresponds to an invariant, and each is the thing that goes wrong when a hurried change adds a shortcut — which is why they are worth writing down rather than leaving as an absence.
| From | On | To | Guard | Effect |
|---|---|---|---|---|
| available | reserve(orderId, qty) | reserved | available >= qty, evaluated in the same statement that decrements | available -= qty; expiresAt = now + 15m |
| reserved | orderPaid | confirmed | now < expiresAt AND the reservation still belongs to this order | promised += qty; reservation closed |
| reserved | expiry job OR customer cancels | released | not already confirmed — checked and written atomically | available += qty |
| reserved | reconciliation detects promised > available | oversold | only reachable through a bug or a leak | alert, and the order is held rather than cancelled |
- released → confirmed — Stock was returned to the pool and may already have been reserved by someone else. Confirming after release is how one unit gets promised twice, and it is exactly the race between a late payment webhook and the expiry job — which is why the release must be conditional and atomic rather than a read followed by a write.
- confirmed → released — The stock is committed to a paid order. Releasing it means the customer paid for something the warehouse will give to someone else. A cancellation after confirmation is a different operation entirely — a return — with its own accounting, and collapsing the two is a common and expensive modelling error.
- available → confirmed — Skipping the reservation means the availability check and the commitment are two separate operations with a gap between them, which is precisely the read-check-write race the reservation exists to close. Any "fast path" that does this reintroduces double-selling under exactly the load where it hurts most.
- oversold → confirmed — Auto-confirming out of an oversold state destroys the evidence of how it happened. Oversold exists to be investigated; a transition that quietly resolves it turns a detectable bug into a permanent silent one (Swallowed Errors).
Note what oversold is doing here. It is not a state the design permits — it is a state the design *names*, so that reconciliation has somewhere to put a violation and a human has something to look at. A system with no name for its illegal states discovers them as support tickets (Making Illegal States Unrepresentable argues for removing states; this is the complementary move for the ones you cannot remove).
What the boundary buys, priced
The change worth pricing is the one that is coming: inventory moves to the warehouse team. This is where a consistency boundary either was or was not in the right place, and there is no third option — an invariant spanning the split is discovered as a rewrite.
Inventory becomes a service with its own database, owned by a different team on a different release cadence. Order management stays where it is.
Every invariant that spanned the two systems has to be restated as eventually consistent, each needing a window, a compensation and a reconciliation that did not exist. Reporting queries that joined orders to stock stop working and need a different answer. The work is not parallelisable and it cannot ship in slices, because the transaction either spans two databases or it does not.
The invariant never spanned the split, so nothing about the invariant changes. What changes is that the reservation call can now time out or answer twice, which the design already had to handle because the expiry job made it possible for a reservation to vanish at any moment.
How to build it
Most important first.
- For each invariant, list the facts it relates and ask whether a reader could see them disagree. That question, not "what is an aggregate", is what decides the boundary (The Aggregate Root).
- Keep boundaries as small as the invariants allow. A boundary is a contention unit as well as a consistency unit, and every extra thing inside it is something the next writer has to wait behind (Concurrency & Parallelism owns the locking mechanism).
- Where two boundaries must coordinate, model the intermediate state explicitly —
reservedrather than a hidden gap between two writes — so the in-between is a state the system understands rather than an accident (State Machines). - Give every cross-boundary flow a timeout and a compensation, because "the second half did not happen" is not an edge case at a boundary, it is a normal Tuesday (Designing for Failure).
- State the consistency window out loud, in the code and to the business. "Stock is accurate within fifteen minutes" is a design; "usually accurate" is a hope (API Design covers how you state that to a client).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Two boundaries with an explicit reservation: moving inventory to the warehouse team's own service costs replacing a function call with a network call behind the same reservation interface, plus retries and a timeout. Days, not quarters, because the invariant never spanned the split.
- One large transaction: the same move is a redesign. Every invariant that spanned the two systems must be restated as an eventually-consistent one with a compensation, which means revisiting checkout, refunds, cancellations and the reporting that assumed a consistent read. Quarters, and it is the kind of project that gets abandoned halfway (Incremental Migration).
- Under the two-boundary design, adding a new participant — a fulfilment hold, a gift-card reserve — costs a new reservation of the same shape, reusing the expiry and compensation machinery. Under the single-transaction design it costs another statement inside a transaction that is already too long, and the cost lands on latency rather than on the change itself, which is why it keeps being paid.
- What the two-boundary design costs on every change: every feature that touches both now has to think about the window, and every reader has to be told which of the two facts it is looking at. That is a permanent tax on ordinary work, and it is the honest reason people reach for the single transaction.
- Small boundaries buy concurrency and evolvability and cost correctness guarantees you then have to reconstruct with reservations, timeouts and compensation — which is more code and more failure modes than one transaction.
- Explicit intermediate states make the system honest and make every reader deal with a state that used to be invisible.
- And every eventually-consistent decision is a promise to operate something forever: an expiry job, a reconciliation, an alert. Design cost is one-time; operational cost is not.
What can go wrong
- The boundary grows until it is a lock on the busiest table in the system, and the fix is a rewrite of checkout rather than a tuning change.
- The intermediate state is implicit — a row written here and not yet there — so a crash leaves a state nothing in the system has a name for, and the recovery code has to guess (Optional Values and Absence).
- Reservations are created and never released because the expiry job was written, deployed and then quietly stopped running; stock drifts to zero while the warehouse is full.
- The eventual-consistency window is stated at fifteen minutes and never measured, so nobody notices when it becomes four hours during a backlog (Observability & Performance owns the backlog signal).
- And the mitigation's own failure: a compensation that itself fails leaves a partially-compensated state, which is strictly worse than the original inconsistency because it is now in a state no one modelled.
- Everything inside a boundary is coupled hard, deliberately, and that coupling is the price of the guarantee (Shared-State Coupling).
- Cross-boundary coordination introduces a dependency on time — a reservation expiring — which means a clock and therefore an injected one (Time as a Dependency).
- A boundary spanning two storage systems introduces a dependency on distributed coordination, which is where this domain hands over (Distributed Systems owns what that costs).
- "Aggregates are a DDD thing, so this only applies if we do DDD." The question — can a reader observe these facts disagreeing — is independent of any methodology. Aggregates are one vocabulary for the answer (Aggregates).
- "Smaller boundaries are better." Smaller boundaries move work from the database to your code, and your code is worse at it. A boundary that is too small produces reservations, sagas and reconciliation for an invariant a single transaction would have held for free (Over-Decomposition).
- "Eventual consistency means we do not need a boundary." It means the boundary is a *window* with a stated size and a remediation. An unstated window is not eventual consistency, it is a bug with a fashionable name (Distributed Systems owns the theory).
- "One transaction is always simpler." It is simpler to write and it silently commits you to one datastore for everything inside it — which is the most expensive architectural decision in the list and the one most often made by accident (Design, Architecture and System Design).
- god-object
- temporal-coupling
Testing it, and how it ages
- Test each boundary's invariant in isolation, as a property over sequences of operations (Property-Based Testing).
- Test the cross-boundary flow with the second half failing, then assert the compensation ran and the state is one the state machine knows about (Where a Test Must Be Real).
- Test reservation expiry with an injected clock, including the race where a confirmation and an expiry arrive together — that one is a real bug in most implementations (A Deterministic Core).
- Assert the consistency window in production with a measurement, not in a test. A window is an operational promise and tests cannot keep it (Debuggability by Design).
- Consistency boundaries are the most durable structural decision in a codebase. Modules get renamed, layers get restyled, frameworks get replaced — and the set of things that must change together stays the same because it follows from the business rules.
- That durability is why they are the right thing to plan a service split around, and why a split that ignores them fails: the invariant does not disappear, it just becomes a distributed transaction nobody signed up for.
- They change when the business changes what it is willing to promise — "we may oversell by 2% and refund" is a boundary change disguised as a policy decision, and it is usually a good trade the engineers were never asked about.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThat an invariant relating two facts forces them to change together follows from what the invariant claims, so it holds in any language and any storage technology; only the mechanisms for achieving atomicity differ.
- SCALE-SPECIFICIn a single-node system with modest concurrency, one large transaction genuinely is the right answer and the reservation machinery is waste — the boundary question only starts paying at the point where contention or an intended split makes the transaction expensive. Where it flips is usually contention on a hot row, not total system size.
- CONTESTEDThe strongest opposing view: most teams split consistency boundaries far too eagerly, on the strength of a scaling story that never arrives, and end up hand-writing sagas, reconciliation and compensating transactions to reimplement — badly, and with more failure modes — what a single database transaction gave them for free. The empirical record supports this: a large fraction of distributed-transaction complexity in the industry exists to serve a split that was never needed. The counter is narrow and specific: the boundaries this lesson argues for are drawn by observable invariants rather than by anticipated scale, so the recommendation is to *know* where they are, keep them as small as the invariants allow, and split only when contention or ownership actually forces it — not to split now.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — how the same boundaries become service boundaries under load, and why a system split along a line an invariant crosses ends up reimplementing transactions over the network.