FramingGENERALDOMAIN-SPECIFICILLUSTRATIVE

What Must Be True?

For "checkout works" to be true, a set of smaller things must each be true: the cart total is right, stock exists, payment succeeded exactly once, an order was recorded. Decompose the frame into conditions, and each condition is a test and a place a design decision lives.

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

The frame says what the system does. How do you turn "it works" into the list of things that must each hold, and what does that list give you that the frame did not?

The situation

The frame says "customer checks out and pays". You have built a checkout that does that on the happy path. A reviewer asks "how do you know it works?" and you realise your answer is that you clicked through it once and an order appeared.

The reflex

Test the flow you built. Click through it, watch the order appear, maybe write an end-to-end test that does the same. "It works" means "the thing I built does what I built it to do", and the test proves that.

Why it stalls

The test proves the code matches your understanding, not that your understanding matches the problem. If you never thought about the price changing mid-checkout, neither did the test, and both pass.

What the reflex produces — and fails to produce
  • The test proves the code matches your understanding, not that your understanding matches the problem. If you never thought about the price changing mid-checkout, neither did the test, and both pass.
  • Conditions that live outside the happy path have no home. "Payment succeeded exactly once" is not a step in the flow; it is a property of the flow under retries, and a click-through never exercises it.
  • Design decisions are made implicitly. Where the stock check lives, whether the total is recomputed at payment, whether the order is written before or after the charge — each was decided by whatever the code did first, and none was written down as a condition to uphold.
  • "It works" cannot be argued about because it has no parts. When something breaks, nobody can say which condition failed, because the conditions were never named.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Take the frame's statement — "checkout works" — and ask what must be true for it to be true. Not what steps happen, but what conditions hold at the end and throughout. Then ask the same of each condition until you reach things that could be observed directly. The result is a tree whose leaves are conditions, each checkable.
  • Distinguish conditions from steps. "Charge the card" is a step; "the customer is charged exactly once for this order" is a condition. Steps are how; conditions are what must hold regardless of how. A tree of steps is a flowchart; a tree of conditions is a specification you can test against and design from.
  • For each leaf, name where in the system it is enforced and how you would observe it. "Stock never below zero" is enforced by a database constraint and observed by a concurrent test; "the price paid is the price shown" is enforced by snapshotting prices into the order and observed by changing a price mid-checkout. The leaf that has no enforcement point is a design decision not yet made (Invariants: Name It Before You Lock It).
  • Mark which leaves are invariants — must hold always — and which are requirements of this flow. Invariants get tests that run forever and constraints that live as close to the data as possible; flow requirements get tests for the flow (Invariants as Tests).

"Checkout works", decomposed into conditions

The tree below decomposes the claim, not the flow. Every leaf is a condition with an observation that would show it holds — or fails. The leaves that read "enforced by" a database mechanism are the invariants; the others are requirements of this flow.

What must be true for checkout to work
Checkout works
  • The cart is valid at checkoutnothing downstream can be right if the input is wrong
    • Every item is purchasabletestable Checkout with a discontinued item is rejected with a reason naming the item.
    • Every item is in stock when the order is confirmedtestable Two concurrent checkouts for the last unit: one succeeds, one is rejected, stock never reads below zero. Enforced by a database constraint.
  • The money is rightthe conditions a customer would sue over
    • The total charged equals the total showntestable Change a product's price after the cart was viewed and before payment; the charge matches what was shown. Enforced by snapshotting prices into the order.
    • The customer is charged exactly oncetestable Submit payment twice, and replay the provider's confirmation; one charge, one paid order. Enforced by an idempotency key and a stored confirmation id.
  • The record is rightthe order is what everything after checkout reads
    • An order records what was bought at what pricetestable The order's items and prices match the cart at checkout time, not the catalogue now.
    • No partial state survives a failuretestable Kill the process between charge and order write; on recovery there is either a paid order or a refunded charge, never a charge with no order.
  • The customer knows the outcometestable After success, a confirmation with the order id; after any failure, a message that says what happened and what to do next.

Seven leaves. The one about partial state is the hardest to enforce and the one most tempting to leave off; it is on the tree because it is true of a working checkout, whether or not it is convenient.

Condition or step?

The same checkout written as steps and as conditions. Steps are what the code does; conditions are what must be true whether or not the code did it in that order. The matrix shows why a tree of steps cannot be tested for the things that matter.

As a stepAs a conditionWhat the condition catches that the step does not
Check stockEvery item is in stock at the moment the order is confirmedThe item that went out of stock between the check and the confirmation.
Compute the totalThe total charged equals the total shownA price changed by the admin mid-checkout; a discount applied twice.
Charge the cardThe customer is charged exactly once for this orderA double-click; a retried request; a replayed provider confirmation.
Write the orderAn order exists that records what was bought at what priceA crash after the charge; an order written from the current catalogue instead of the cart.
Show the confirmationThe customer is told the outcome, including on failureA failed payment that shows a blank page; a success that never confirms.

The conditions as assertions

The leaves, written as the checks a test would make. Pseudocode, because the point is the shape — each condition becomes an assertion with a setup that attacks it — and not the syntax. Notice that none of these tests is the happy path.

The condition tree as tests
1test "in stock at confirmation":
2 stock(product) = 1
3 run checkout(cartA with product) and checkout(cartB with product) concurrently
4 assert exactly one succeeded
5 assert stock(product) = 0 -- never negative
6
7test "total charged equals total shown":
8 view cart -> shown = 10.00
9 admin sets product.price = 12.00
10 pay
11 assert charged = shown
12
13test "charged exactly once":
14 submit payment twice with the same idempotency key
15 replay the provider's confirmation event
16 assert charges(order) = 1
17 assert order.status = PAID
18
19test "no partial state":
20 crash after charge, before order write
21 recover
22 assert (paid order exists) or (charge refunded)
23 assert not (charge exists and no order)

Each test is a condition attacked. The last one is the hardest to write and the one that most often reveals that a design decision — where the order is written relative to the charge — was never made.

How to do it

Most important first.

  • Write the frame's claim at the root. Below it, write "this is true only if…" and list the conditions. Repeat for each condition until every leaf is something you could observe in a running system.
  • Rewrite any leaf that is a step ("call the provider") as a condition ("the provider has confirmed the payment, and we hold the confirmation id").
  • For each leaf, write two things beside it: where it is enforced, and how it would be observed failing. A leaf missing either is work to do (Finding Invariants From Examples).
  • Ask of each leaf: does this have to be true always, or only for this flow? The "always" ones are invariants, and they are what the failure-modelling module will inject against later (What Must Never Break).
  • Turn the leaves into tests in the order of what would cost most if false. Exactly-once payment before correct total formatting.

Worked on a concrete problem

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

  • "Checkout works" is true only if: the cart contains only purchasable items at the moment of checkout; the total charged equals the sum of the prices shown, plus any shipping, and nothing else; every item is in stock at the moment the order is confirmed; the customer is charged exactly once; an order exists that records what was bought at what price; the customer is told the outcome; and if any of these fails, no partial order or partial charge remains. Seven conditions, none a step, each observable.
  • The leaf that found a design decision. "The total charged equals the sum of prices shown." Where enforced? Nowhere, yet — the checkout recomputed prices from the product table at payment time, so a price change by the admin between cart and payment charged a different amount from the one shown. The condition forced a decision: snapshot prices into the order at checkout start, and charge from the snapshot (Snapshots vs References).
  • The chat app. "Read receipts work" is true only if: a message is marked read only after the reader's client has displayed it; a read mark is never lost once recorded; a read mark from one device is visible from the other; and the sender never sees "read" for a message the reader has not seen. The last condition was the one the one-device assumption in the frame had hidden, and it became the reason the frame changed.

How you know it worked

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

  • A tree exists whose root is the frame's claim and whose leaves are conditions, not steps, each with an enforcement point and an observation.
  • At least one leaf had no enforcement point when first written, and a design decision was made to give it one.
  • The invariants are marked and have tests that would fail if they broke, independent of the happy-path flow.
  • When something breaks, you can name the leaf that failed.

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
  • ?This is true only if what else is true — and is each of those a condition or a step?
  • ?For each condition, where in the system is it enforced, and how would I observe it failing?
  • ?Which of these must hold always, and which only for this flow?
  • ?Which condition, if false, would cost the most — and is it the first one I test?

What can go wrong

How the move itself fails
  • The tree becomes a flowchart. Leaves like "call the payment API" and "write the order row" are steps, and a tree of steps only checks that the steps ran. Rewrite them as what must be true after the step.
  • The tree is exhaustive. Forty leaves for checkout, including "the confirmation page uses the right font", and the seven that carry the risk are lost among them. Decompose until the leaves are observable, then stop; prune by cost-if-false.
  • Leaves are written and not enforced. A condition on paper with no constraint, no test and no code path is a wish; the point of naming it is to give it a home.
  • The move is applied before the frame exists. Conditions decompose a claim; without a claim there is nothing to decompose, and the tree becomes a list of things that might matter.
What the move costs
  • A condition tree takes longer to write than a happy-path test, and on a flow with no money and no concurrency it may name nothing the happy path did not cover.
  • Every leaf with an enforcement point is a constraint the code must now respect, and constraints slow down the next change. That is what they are for, and it is still a cost.
  • Conditions written before the flow is built can be wrong about what is observable; some will need rewriting once the system exists.
Misreads
  • "What must be true is the same as the acceptance criteria." Acceptance criteria describe the flow from outside — given, when, then. Conditions describe what holds inside, including under retries and concurrency, which no given-when-then exercises unless someone thought of it first.
  • "Every leaf is an invariant." Some are requirements of this flow — "the customer is told the outcome" — and can change with the flow. Invariants are the subset that must hold regardless of flow, and they get stronger enforcement.
  • "This replaces failure modelling." It precedes it. Failure modelling asks what happens when each condition is attacked; the conditions have to be named first, and this is where they are named (Failure Modeling).

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.

  • GENERALAny claim about a system decomposes into conditions; the tree is the same shape for a checkout, a message pipeline or a compiler pass, and what differs is which leaves are invariants.
  • DOMAIN-SPECIFICWhere money, identity or safety are involved, the invariant leaves dominate and each needs enforcement close to the data. In a read-only dashboard nearly every leaf is a flow requirement, and the tree is mostly a test plan.
  • ILLUSTRATIVEThe checkout, the admin price change mid-checkout and the read-receipt conditions are invented to show the decomposition; no real store is described.

Where the depth lives

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

Further
  • A Testing & Reliability domain would take the assertions here and teach how to run them under real concurrency and real crashes; until then, the failure module's injection lessons are the nearest thing.