InvariantsGENERALDOMAIN-SPECIFICILLUSTRATIVE

What Must Never Break

A feature list says what the store does. An invariant says what it must never do — an order total is never negative, a payment never happens twice — whatever feature, bug or concurrent user is involved. Finding them is a different question from finding requirements, and it is asked before the data model.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

You have a list of what the store should do. What is the separate list of things that must never be true, and how do you find it before the code decides it for you?

The situation

I have the requirements written out — browse, cart, checkout, pay, admin edits stock. It feels complete. Then a colleague asks "what stops an order from being paid twice?" and I realise nothing in my list says it can't, and I have no idea where that rule would even go.

The reflex

Add validation. Every input gets checked at the edge — quantity must be positive, price must be a number, the order must exist — and each check feels like it closes a hole. Validation is visible, cheap and immediately testable, so the list of checks grows and looks like safety.

Why it stalls

Input validation protects one entry point. The rule "an order total is never negative" has to hold after a refund, after a price change, after an admin edits a line, after a retry — and the validation on the checkout form knows about none of those. The checks multiply and the property is still not stated anywhere.

What the reflex produces — and fails to produce
  • Input validation protects one entry point. The rule "an order total is never negative" has to hold after a refund, after a price change, after an admin edits a line, after a retry — and the validation on the checkout form knows about none of those. The checks multiply and the property is still not stated anywhere.
  • The list of checks is derived from the inputs that exist today, so it says nothing about the inputs that arrive next month. When the refund endpoint is added, nobody re-derives the checks, because nobody wrote down what they were protecting.
  • The most important properties are not about inputs at all. "A payment happens at most once per order" is violated by two correct requests arriving together, and no field validation can see that. The reflex produces a wall of checks and leaves the real property undefended.
  • When asked "what must never break?", the honest answer after a day of validation is "whatever the checks happen to cover" — which is a description of the code, not of the problem.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Separate two questions that feel like one. "What should the system do?" produces requirements — features, workflows, actions. "What would make the system *wrong* even if every feature worked?" produces invariants — properties of the state that must hold at every observable moment regardless of which feature, bug, retry or concurrent user touched it. The second question is asked of the data and the money, not of the screens.
  • Walk the entities and ask each one what it would be embarrassing for it to say. An order whose total is negative; a payment that refers to no order; stock below zero; a cart item whose product was deleted; an order that is both cancelled and shipped. Each embarrassment, negated, is a candidate invariant. Most will be dull; the dull ones are the ones that get violated.
  • For each candidate, ask what could violate it: which actions touch that state, and whether two of them could touch it at once. That question sorts invariants into ones a database constraint can hold, ones a single transaction can hold, and ones that need a decision about concurrency (Invariants Under Concurrency). It also reveals the actions you had not listed as requirements.
  • Write the invariants next to the requirements, as sentences with "never" or "always" and a subject that is a piece of state. They are not features and they are not tests yet; they are the constraints the design has to satisfy, and they will decide where state lives (Who Owns This State?) and what a transaction boundary has to cover before any of that is coded.

A different question from "what should it do?"

Requirements are produced by asking what the system does; invariants by asking what the state must never say. The two questions sound similar and lead to different places, and the ladder below shows the same worry asked three ways. The vague form invites a list of checks; the best form names a piece of state, the actions that touch it and the property that must survive them — which is exactly what the design needs to know.

The Software Engineering & Design domain teaches what an invariant is and how code enforces one (Invariants: Name It Before You Lock It, Enforcing Invariants). This lesson is the step before: noticing that the question needs asking, and asking it of each entity before the schema exists.

The same worry, three ways
vagueHow do I make sure the order data is valid?
betterWhat should never be true about an order?
bestWhich properties of an order must hold after every action that can change it — checkout, refund, admin edit, retry — and which of those actions can run at the same time?

why The best form names the state, enumerates the actions that threaten it and asks about concurrency, so it can be answered by listing invariants with their violators; the vague form can only be answered with more validation, and the middle form produces properties with no idea of what could break them.

Where each invariant comes from, and where it goes

Walking the store's entities produces candidates; sorting them by what could violate them decides their home. The matrix is the sorting: a property touched by one action at one entry point is a validation problem; a property touched by several actions is a design rule that has to be held somewhere all of them pass through; a property whose actions can run concurrently needs a decision the next lesson but one is about.

The last column is the one that matters for what to build first. An invariant whose violation touches money or the outside world cannot be repaired by fixing rows afterwards, and those are the ones to design for before the first real customer, not after.

InvariantActions that could violate itConcurrent?Likely homeRepairable afterwards?
Order total is never negativecheckout, refund, admin line editrarelya rule where totals are computed, plus a check constraintyes — recompute
Every order item references an existing productcheckout, product deletionyes — delete during checkoutforeign key; product soft-delete decisionyes, awkwardly
A payment succeeds at most once per ordercheckout, double click, provider retryyes — that is the whole problemidempotency key + unique constraint on (order, success)no — money moved
Stock is never negativecheckout, admin edit, refund restockyes — two checkoutsa transaction that checks and decrements together, or a check constraintno — the unit was promised
Captured price never changes after checkoutadmin price editnoschema: snapshot on the order item, not a referenceno — the customer saw a number

How the list goes wrong

The table below is what happens when the second question is not asked, or is asked and then not attached to anything. Each row is a real shape: the symptom is what the customer or the support team sees, and the cause is usually not the code that failed but the property nobody wrote down.

Notice that in every row the code that "failed" was correct for the input it received. That is the signature of a missing invariant: no single function is wrong, and the state is.

Invariants that were never named
TriggerSymptomCauseResponse
A refund is added months after checkoutOrders with negative totals appear in reportsTotal was validated at checkout, never stated as a property; the refund code subtracted freelyState the invariant, hold it where totals are computed, add the constraint, backfill
Provider retries its payment confirmationOne order shows two successful payments; the customer is charged twice"At most one" was assumed, not designed; nothing made the second confirmation a no-opIdempotency on the confirmation (Duplicate Requests), unique constraint, refund the duplicate
Admin deletes a product that is in a live cartCheckout crashes on a null productThe reference invariant existed only as a foreign key in one direction; the cart had noneDecide soft-delete vs cascade explicitly; hold it in the schema
Two customers buy the last unitStock reads minus one; two shipments promisedThe check and the decrement were two statements; "never negative" was a form checkCheck and decrement in one atomic step (Invariants Under Concurrency)

How to do it

Most important first.

  • For every entity you have named, write down one thing it must never say about itself. If nothing comes, the entity is either trivial or you do not yet understand it — both are worth knowing.
  • Write each invariant as a property of state, not as a check on an input: "stock is never negative", not "reject negative quantities". The first survives the addition of a new endpoint; the second does not.
  • Attach to each invariant the list of actions that could break it. An invariant with one action is a validation problem; one with several is a design problem; one whose actions can run concurrently is a concurrency problem (Invariants Under Concurrency).
  • Mark which invariants are about money or the outside world — a payment taken, an email sent, stock promised. Those are the ones whose violation cannot be undone by fixing the data afterwards.
  • Hand the list to the data model: a constraint the database can hold should be held there (Where Invariants Live), and the rest tells you where the transaction boundaries and the ownership decisions are.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • The store's requirements say: customer adds to cart, checks out, pays; admin edits price and stock. Asking each entity what it must never say: Order — "my total is negative", "my items reference a product that does not exist", "I am paid but no payment record exists". Payment — "I belong to no order", "there are two of me for the same order and both succeeded". Inventory — "I am below zero", "I was decremented for an order that was never created". Cart — "I contain a product that no longer exists at a price that no longer exists".
  • Sorting by what could violate them: "total never negative" is touched by checkout, refunds and admin line edits — three actions, so it is a design rule, not a form check. "Stock never negative" is touched by checkout and admin edits, and two checkouts can run at once — a concurrency problem, flagged for Invariants Under Concurrency. "Payment at most once per order" is touched by checkout, by the customer clicking twice and by the provider retrying its confirmation — it needs an idempotency decision, not validation.
  • Two of the candidates turned out not to be invariants. "An order always has at least one item" is true at creation and false after a full refund in some designs — so it is a decision to make, not a property to protect. "A product's price never changes" is false; what is true is "an order's captured price never changes after the order is created" (Snapshots vs References). Finding that distinction before the data model saved a wrong foreign key.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • A second list exists beside the requirements, and every entry on it has a subject that is state, a "never" or "always", and the actions that could violate it.
  • At least one entry has surprised you — a property you had not considered protecting, or a "requirement" that turned out to be a decision.
  • You can point at each invariant and say where it will be held: database constraint, transaction, application rule, or "not yet decided, and here is the question" (What Must Be True?).
  • When a new feature is proposed, the first question you ask is which invariants its actions touch — and the answer is fast, because the list exists.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?For each entity, what would it be wrong for it to say about itself, whatever feature produced it?
  • ?Which actions can touch this piece of state, and can two of them touch it at the same moment?
  • ?Is this a property that must always hold, or a decision I have not yet made about what should happen?
  • ?Which of these invariants involve money or the outside world, and therefore cannot be repaired by fixing a row afterwards?
  • ?Where will each invariant be held — the database, one transaction, or a rule I have to design — and which ones have no home yet?

What can go wrong

How the move itself fails
  • Everything becomes an invariant. "The product name is never empty" is true and harmless and worth a column constraint; it does not belong on the same list as "a payment never happens twice". A list of forty invariants with no ranking is ignored the way a list of forty warnings is.
  • Invariants are written and then never attached to the actions that threaten them. The property "stock never negative" on a page proves nothing; the design that keeps it true under two concurrent checkouts is the work, and the sentence is only the pointer to it.
  • The move is applied to a prototype whose data will be thrown away. A spike that answers "can this provider do hosted checkout?" has no invariants worth writing; the time is better spent on the question the spike exists to answer.
  • Invariants replace requirements. A system that never violates anything and also does nothing useful is easy to build; the list is a constraint on the design, not a substitute for deciding what the store does.
What the move costs
  • Time spent on the invariant list is time not spent on the first slice, and on a throwaway prototype it is time wasted.
  • Naming an invariant commits you to defending it. "Stock never negative" sounds free until it forces a decision about concurrent checkouts that a first version could have deferred by allowing oversell and apologising.
  • Invariants held in the database are the hardest to change later; deciding early to hold one there is a small irreversibility, chosen on purpose.
Misreads
  • "Invariants are just validation with a fancier name." Validation checks an input at one edge; an invariant is a property of state that every edge, every retry and every concurrent action must preserve. The reflex above shows how the first fails to produce the second.
  • "If the database has constraints, the invariants are handled." Constraints hold the invariants a single row or a foreign key can express. "A payment happens at most once" across a retrying provider and a double-clicking customer is not one of them; neither is "the captured price never changes after checkout" unless you designed the schema to make it so.
  • "An invariant is something we will never change." It is something the *system* must never violate; the list itself changes when the requirements do. When multiple warehouses arrive, "stock never negative" becomes "stock at each warehouse never negative", and knowing the invariant is why you know what has to change (When Assumptions Change).

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALEvery system with state has properties that must hold across all operations on it; the question "what would make this wrong even if every feature worked?" applies to a compiler's symbol table as much as to a store's orders.
  • DOMAIN-SPECIFICIn a payments or inventory system the invariants are the design and are found first; in an internal dashboard that reads from someone else's data most invariants belong to the upstream system, and the useful list is short — usually "never show a number that mixes two sources".
  • ILLUSTRATIVEThe store, its entities and the colleague's question are invented to show the shape of the move; the number of invariants a real store carries depends on its business rules.

Where the depth lives

This domain asks the question and hands the answer off by name.

Further
  • A testing domain would teach how to turn each row of the table into a property test; until it exists, Invariants as Tests covers the thinking and the Design lesson Property-Based Testing covers the technique.