InvariantsGENERALSCALE-SPECIFICCONTESTED

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.

The question

Which pieces of state have to move as one, and what happens to the ones I decide do not?

The requirement

"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.

The obvious build

Put both in one transaction. They are both invariants, transactions are what invariants use, and the database will sort it out.

Why it breaks

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).

How it breaks as requirements change
  • 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.
RequirementConstraintsInvariantsResponsibilitiesBoundariesInterfacesStateDependenciesFailureImplementationTestsFeedbackEvolution

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.

Constraints
  • 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).
Invariants
  • 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.

Responsibilities
  • 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.
Boundaries
  • 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).
Two boundaries, joined by a reservation rather than a transaction
1. reserve, 15 mindecrements available2. create order atomically3. confirmreleases unconfirmedpromised vs available, every 5 minCheckoutExpiry jobOrder + lines + total (one transaction)Reservation (explicit, expiring)Inventory (its own boundary)Reconciliation + alert
UserLLMAgentToolDataDecisionHumanGuardrail

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.

A stock reservation
availablereservedconfirmed ·released ·oversold ·
FromOnToGuardEffect
availablereserve(orderId, qty)reservedavailable >= qty, evaluated in the same statement that decrementsavailable -= qty; expiresAt = now + 15m
reservedorderPaidconfirmednow < expiresAt AND the reservation still belongs to this orderpromised += qty; reservation closed
reservedexpiry job OR customer cancelsreleasednot already confirmed — checked and written atomicallyavailable += qty
reservedreconciliation detects promised > availableoversoldonly reachable through a bug or a leakalert, and the order is held rather than cancelled
must be impossible
  • released → confirmedStock 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 → releasedThe 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 → confirmedSkipping 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 → confirmedAuto-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 moves to a separate service owned by the warehouse team
The change

Inventory becomes a service with its own database, owned by a different team on a different release cadence. Order management stays where it is.

One transaction spanning orders, lines and inventory
CheckoutFlowOrderServiceInventoryLogicRefundFlowCancellationFlowReportingQueriesAdminOrderToolthe transaction boundary itself
testscheckout_testrefund_testcancellation_testreporting_testadmin_testand every test that relied on a consistent read across both
8 modules · 6 test files

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.

Two boundaries joined by an explicit reservation
the reservation client — a function call becomes a network call
testsreservation_client_testreservation_timeout_test
1 module · 2 test files

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.

what it cost The reservation design has been charging for this the whole time and it is worth naming precisely. Stock shown to a customer has been approximate since day one, so the UI has had to say "3 left" carefully and support have had to explain a rare oversell. There is an expiry job that must run, be monitored and be alerted on — permanently, including in every developer environment where its absence produces confusing test failures. Every reader that wants a true stock number has to go through the reservation view rather than reading a column. And two extra states exist that a single transaction would never have needed. That is several months of accumulated friction, paid in exchange for one migration being days rather than quarters — a trade that is clearly right if the split happens and clearly wrong if it never does.

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 — reserved rather 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.

Cost of the next change
  • 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.
What the recommended approach costs
  • 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

Failure modes
  • 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.
Dependencies, and their direction
  • 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).
Misreads
  • "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).
Smells this explains
  • god-object
  • temporal-coupling

Testing it, and how it ages

What to test, and at which boundary
  • 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).
How this design ages
  • 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.

Domains that do not exist yet
  • 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.