InvariantsGENERALILLUSTRATIVETEAM-SPECIFIC

Finding Invariants From Examples

Abstract state is hard to reason about; a concrete case is not. Construct a small example — three units, two buyers, one price change — walk it step by step, and the property that must hold falls out of the moment the example goes wrong. Counterexamples find invariants faster than definitions do.

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 cannot see what the invariants of a system are by staring at its entities. How do you construct examples that make the invariants visible — and the ones that are not really invariants fall away?

The situation

I know I am supposed to list "what must never break", and I have the entities in front of me, and nothing comes. Order, Payment, Inventory — they just look like tables. Everything I write down is either obviously true or obviously a guess.

The reflex

Think harder in the abstract. Reread the entity list, try to imagine every property each one could have, and write definitions: "an order is consistent if…". It feels rigorous, and a page of definitions looks like the list is being found.

Why it stalls

Abstract reasoning about state produces the properties you already believed. The invariants that matter are the ones you did not know you were assuming, and they do not appear by introspection — they appear when a concrete case violates them.

What the reflex produces — and fails to produce
  • Abstract reasoning about state produces the properties you already believed. The invariants that matter are the ones you did not know you were assuming, and they do not appear by introspection — they appear when a concrete case violates them.
  • The definitions are unfalsifiable as written. "An order is consistent if its items and totals agree" cannot be checked against anything, so it is not obviously wrong, so it stays on the page and nobody discovers it is incomplete until a refund arrives.
  • The page of definitions is motion: it looks like the list, it took an afternoon, and it does not name a single action that could break anything. When the design starts, it is set aside, because it does not say what to do.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Construct a small, fully concrete example of the state and the actions on it — named people, small numbers, specific prices — and run the actions by hand, writing the state after each step. The example must be small enough to hold entirely in your head; the point is that you can see every field.
  • At each step ask: is there anything about this state I would be unhappy to show a customer or an accountant? The moment you would be unhappy is an invariant being violated, and negating the unhappiness gives its statement. If nothing is ever unhappy, change the example: add a second actor, a retry, a failure between two steps, an admin editing during the workflow.
  • Then try to construct a counterexample to each candidate invariant: a sequence of legitimate actions that ends in a state violating it. If you find one, either the property is not an invariant (it is a decision you have not made) or you have found the design work — the actions that must be made atomic, ordered or rejected. If you cannot find one, keep the invariant and keep the counterexample attempt; it is the seed of the test (Invariants as Tests).
  • Vary one thing at a time and watch which properties survive. Two buyers instead of one exposes concurrency; a failure between "charge" and "create order" exposes atomicity; a price change mid-checkout exposes snapshot-versus-reference. Each variation is a thought experiment (Thought Experiments) and each takes minutes.

The loop: example, disturbance, unhappiness, negation, attack

The pipeline is deliberately mechanical. Each stage has one output, and the stage most people skip — negating the unhappy moment into a property — is the one that turns a story into an invariant.

The attack stage is where candidates die, and it should kill some. A candidate list that survives every counterexample attempt on the first pass was probably attacked too gently.

From a blank entity list to invariants with violators
  1. 1
    Construct

    One product, one or two actors, small numbers, real prices, written as a table.

    fails by The example is abstract ("some products, some users") and nothing can go visibly wrong.

  2. 2
    Trace

    Run the happy path; write the state after each step.

    fails by Steps are skipped because they are "obvious", and the obvious step is where the state went wrong.

  3. 3
    Disturb

    Add exactly one: second actor, retry, failure between steps, edit mid-flow. Rerun.

    fails by Several disturbances at once, so the violation cannot be attributed to any of them.

  4. 4
    Negate

    Turn the first unhappy moment into "X is never / always Y" with a state subject.

    fails by The story stays a story; the page fills with scenarios and no properties.

  5. 5
    Attack

    Build a counterexample with legitimate actions only. Survivors are invariants; casualties are decisions or design work.

    fails by Attacks use illegitimate actions and everything falls, or no attacks are tried and everything survives.

Each surviving invariant leaves with the disturbance that revealed it attached — that pairing is the seed of its test.

The trace that finds the concurrency invariant

The store example below is written as a trace, not as prose, because the interleaving is the point. Two correct checkouts, each of which would be fine alone, together leave the state somewhere it must never be. The invariant is visible in the last line; the design work it demands is the next lesson but one.

Product A: stock 3. Alice and Bob each buy 2, interleaved
1state: A.stock = 3
2
3Alice: read stock -> 3 (3 >= 2, ok)
4Bob: read stock -> 3 (3 >= 2, ok)
5Alice: stock = 3 - 2 -> 1
6Bob: stock = 3 - 2 -> 1 (Bob used the value he read)
7Alice: create order -> order#1, 2 units, paid
8Bob: create order -> order#2, 2 units, paid
9
10state: A.stock = 1, promised = 4, existed = 3
11
12unhappy at: promised (4) > existed (3)
13negated: units promised by paid orders never exceed units that existed
14also: stock is never negative (would show with "stock = stock - 2" applied twice: -1)
15attack: both checkouts used only legitimate actions -> this is design work, not a decision

Every line Alice and Bob executed was correct. The invariant is about the combination, which is why no single function's validation could have held it.

Candidates that fall, and what replaces them

The value of the attack stage is easiest to see in the candidates it kills. Each of the rows below was a plausible invariant from an abstract session; the counterexample shows it was a decision in disguise or a property about the wrong subject, and the replacement is what the design actually needs.

Abstract candidate vs what the example produced
Candidate from the definitions page
"An order's total always equals the sum of its products' current prices." Counterexample: admin edits A from 10 to 12 after Alice paid 20; the equation now reads 24 and the order is "inconsistent" although nothing wrong happened.
Invariant from the trace
"An order's total always equals the sum of the prices captured on its items at checkout, and a captured price never changes." Survives the price edit, survives a refund, and tells the schema to snapshot prices onto order items rather than reference the product.

The trace shows which value the customer actually agreed to. The abstract candidate referred to the wrong subject — the product's current price — and would have produced a foreign key where a copied value was needed.

How to do it

Most important first.

  • Write the state as a tiny table with real values. "Product A: stock 3, price 10. Alice cart: 2×A. Bob cart: 2×A." Abstract nouns hide the problem; numbers expose it.
  • Run the happy path step by step and record the state after each step. Boring on purpose — it establishes what "fine" looks like so that "wrong" is visible.
  • Introduce exactly one disturbance — a second actor, a retry, a failure, an edit mid-flow — and rerun. Write down the first moment you would be unhappy to show the state to someone.
  • Negate each unhappiness into a sentence with "never" or "always" and a subject that is state. That is the candidate invariant.
  • Attack each candidate with a counterexample using only legitimate actions. A candidate that survives is an invariant; one that falls is either a decision to make or a design to do (Counterexample Thinking).

Worked on a concrete problem

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

  • State: product A, stock 3, price 10. Happy path, Alice buys 2: cart 2×A → checkout → order total 20, stock 1, payment 20 succeeded. Nothing is uncomfortable. Disturbance: the provider's confirmation arrives twice. After the second: one order, two payment records, both "succeeded", total paid 40 for an order of 20. Unhappy. Negated: "the sum of successful payments for an order never exceeds the order total" — sharper than "no double payment", because it also covers partial captures.
  • Same state, disturbance: Bob also buys 2, at the same time. Step through both checkouts interleaved: both read stock 3, both see "enough", both decrement by 2, stock reads minus one, two orders exist for four units of three. Unhappy at "minus one" and again at "two orders for four units". Negated: "stock is never negative", and the sharper "units promised by paid orders never exceed units that existed". The counterexample is the design problem, handed to Invariants Under Concurrency.
  • Disturbance: the admin changes A's price to 12 while Alice's order is being created. Candidate from the abstract session was "an order's total equals the sum of its products' prices". Counterexample: after the edit, that equation reads 24 for an order the customer paid 20 for. The candidate falls — it was never an invariant. What survives is "an order's total equals the sum of the prices captured on its items at checkout", which is a schema decision (Snapshots vs References) that the abstract definition would have got backwards.

How you know it worked

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

  • You have a written example with names and numbers, a step-by-step trace, and at least one step marked as the moment the state went wrong.
  • At least one candidate invariant from the abstract session has been killed by a counterexample and replaced with a sharper one.
  • Each surviving invariant has the disturbance that revealed it attached — which is the beginning of its test and the name of the design problem it creates.
  • The next disturbance to try is obvious, and the example is small enough that trying it takes minutes.

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
  • ?What is the smallest fully concrete example of this state — names, numbers, prices — that I can run by hand?
  • ?What single disturbance would I add to the happy path: a second actor, a retry, a failure between two steps, an edit mid-flow?
  • ?At which step would I be unhappy to show the state to a customer or an accountant, and what is the negation of that unhappiness?
  • ?Can I build a counterexample to this candidate using only legitimate actions — and if I can, is it a design problem or a decision I have not made?

What can go wrong

How the move itself fails
  • The example grows until it cannot be held in the head — five products, four customers, coupons — and the trace becomes a spreadsheet nobody finishes. The move needs the smallest example that shows the property; one product and two buyers is usually enough.
  • Only the happy path is traced. A trace with no disturbance finds no invariants, because nothing goes wrong; the value is entirely in the second actor, the retry and the failure between steps.
  • Counterexamples are constructed with illegitimate actions — "what if someone edits the database directly?" — and every invariant falls. The attack uses only the actions the system offers; direct database edits are a different threat model.
  • The examples are run but the results are not negated into properties, so the output is a list of scary stories rather than a list of invariants with their violators. The negation is the deliverable.
What the move costs
  • A concrete example proves nothing general: it shows one violation, not that there are no others. The invariants you find are bounded by the disturbances you thought to try (Unknown Unknowns).
  • Tracing by hand is slow compared with reading the entity list, and on a domain you already know well the examples will only confirm what you knew.
  • Killing candidates with counterexamples can feel like losing ground; the candidate that fell was never protecting anything, but the page gets shorter.
Misreads
  • "So invariants are found by testing." They are found by thought experiments that look like tests and run in the head; the test comes after, when the property is known. Running the code first shows you what it does, not what it must never do.
  • "If no counterexample was found, the property is proven." It survived the attacks you thought of. Property-based testing (Property-Based Testing) generates attacks you did not think of; it is the natural next step, not a replacement for the by-hand example.
  • "Examples with small numbers do not represent production." The numbers are small so that the interleaving is visible; the property that holds for three units and two buyers is the same property at any size. What changes at scale is how often the disturbance occurs, not whether it can.

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.

  • GENERALConstructing a concrete case and disturbing it works on any stateful problem — a parser, a cache, a scheduler — because the invariants of any of them are visible only when a specific sequence violates them.
  • ILLUSTRATIVEProduct A, three units, Alice and Bob, price ten and the double confirmation are invented; the numbers are chosen small so the interleaving fits in one head, not to describe any real inventory.
  • TEAM-SPECIFICA solo learner traces on paper; a team does the same trace on a whiteboard with the person who knows the business rules present, because the "would I be unhappy to show this?" judgment is theirs, not the engineer's.

Where the depth lives

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

Further
  • The manifesto's "review the LLM's answer" route at /manifesto/review is the same attack stage applied to generated code: construct the example, run it by hand, look for the unhappy moment.