DecompositionGENERALCONTESTEDILLUSTRATIVE

Recursive Decomposition

A child that is still too big is decomposed the same way its parent was. Checkout becomes Load Cart, Validate Items, Calculate Total, Create Payment, Create Order, Confirmation — and the recursion stops where a leaf could be built on Monday and checked on Friday.

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

Once a piece of the problem is still too big to build, how do you split it again — and how do you know when to stop splitting?

The situation

Checkout is a child of the store's tree and you have picked it as the next thing to build. You sit down to start and cannot: "checkout" is still the sentence "turn a cart into a paid order", which is at least four things, each with its own unknowns.

The reflex

Write the checkout function. Start typing checkout(cart) and let the body reveal the steps as you go — load, validate, pay, save. The steps will appear because the code needs them, and the code is the real thing anyway.

Why it stalls

The body reveals the steps in whatever order the code makes convenient, which is usually the order that avoids the hard part. Payment becomes a TODO while order creation is written in full, and the order gets created before anyone has decided whether payment comes before or after it.

What the reflex produces — and fails to produce
  • The body reveals the steps in whatever order the code makes convenient, which is usually the order that avoids the hard part. Payment becomes a TODO while order creation is written in full, and the order gets created before anyone has decided whether payment comes before or after it.
  • Each step's unknowns surface as blocking errors mid-function instead of as questions beforehand. "What if the price changed since it was added to the cart?" appears as a bug, not a decision.
  • The function is the only artefact, so the decomposition lives in its structure and is invisible to anyone who has not read it; the plan cannot be discussed.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Apply to the child exactly the move you applied to the root: ask what it does, in steps someone could observe, and make each step a child. Checkout does six things in sequence — load the cart, validate the items, calculate the total, create a payment, create the order, show a confirmation — and each of those is a candidate leaf.
  • For each new child, run the leaf test: can you name the observation that would show it works, and could you build it without splitting again? If yes, stop. If not, recurse once more. The stopping rule is buildability, not depth.
  • Notice what the recursion surfaces: decisions that were hidden inside the parent. Splitting checkout forces "does the order exist before or after payment?" and "whose price applies, the one at add-to-cart or the one now?" — questions that become the parent's unknowns board.
  • Keep the recursion local. Decompose the child you are about to build to leaves; leave its siblings at one level until you arrive at them, because what you learn building this one will change how you split the next (Requirements Emerge During Implementation).

Checkout, one level down

The same question that split the store — what does it do, observably? — applied to one of its children. The children here are a sequence, because checkout is a workflow; each carries the observation that ends the recursion for it. Create Payment needed one more level, and the tree says why.

Checkout, decomposed
Checkout: turn a cart into a paid order
  • Load carttestable The current customer's lines and quantities are returned; an empty cart is refused with a reason.
  • Validate itemstestable A line whose product is out of stock is reported by product name; a cart with all lines valid passes.
  • Calculate totaltestable Total equals the sum of quantity times current price; a price changed since add-to-cart is used and shown.
  • Create paymenttwo actors — us and the provider — so two leaves
    • Request a chargetestable A test-mode charge for the total is created and its id stored against the order.
    • Learn the outcometestable The provider's confirmation marks the order paid once; a decline marks it failed; a duplicate confirmation changes nothing.
  • Create ordertestable Exactly one order exists with the validated lines and the prices captured at checkout.
  • Confirmationtestable The customer sees the order number and the amount; the cart is empty afterwards.

Whether Create Order comes before or after Create Payment is deliberately not encoded in the tree — it is the first item on the board below.

What the split forced into the open

The most useful product of recursing is not the leaves; it is the decisions that were invisible while the parent was one word. Each is written here as it was first said, then as a question, then with the experiment that would settle it.

Checkout's board, after the split
known
  • Checkout is six observable steps; five are leaves, one needed another level.
  • The provider answers on its own schedule, so "learn the outcome" is a separate leaf from "request a charge".
assumed
  • ~Prices are captured at checkout, not at add-to-cart — to be confirmed with the founder, since it changes what the cart page shows.
unknown → question → experiment
  1. ? When does the order get created?

    becomes Should an order exist before payment is attempted (so a decline leaves an unpaid order to retry) or only after success (so a decline leaves nothing)?

    experiment Sketch both state machines on paper and list what the customer sees on a decline and on a browser close in each; pick the one whose failure states are all recoverable (The Order Lifecycle, Built).

  2. ? Stock.

    becomes Is stock reserved at validate, decremented at create-order, or decremented only when payment succeeds — and what happens to the reservation on a decline?

    experiment One product with stock of one and two checkouts racing; observe which policy lets both succeed and which one strands stock after a decline.

  3. ? Price changes.

    becomes If the admin changes a price between add-to-cart and checkout, which price does the order carry, and is the customer told?

    experiment Add to cart, change the price in admin, check out; decide from what the confirmation page should honestly say.

None of these could be asked while checkout was one word. That is what recursion is for.

The loop, and where it stops

Recursion is the same short loop applied to a child. The pipeline names each step and, in the last column, the way it goes wrong — mostly by not stopping.

Decompose one child
  1. 1
    Pick the child you are about to build

    Not the whole tree; the branch the order of work says is next.

    fails by Recursing into every branch at once, encoding guesses about parts not yet touched.

  2. 2
    Say what it does, observably

    A sequence for a workflow, a set for a capability, alternatives for a decision.

    fails by Listing what the code will contain instead of what the piece does.

  3. 3
    Leaf test each child

    "Works when" comes easily and it is buildable without another split — stop.

    fails by Stopping because the name sounds simple; "create payment" hid a second actor.

  4. 4
    Record the forced decisions

    Each becomes a specific question and an experiment on the parent's board.

    fails by Deciding silently in code; nobody can later say why the order exists unpaid.

  5. 5
    Leave the siblings shallow

    One level, with a note; they will be split when you arrive with what this branch taught you.

    fails by Splitting them now for completeness, then re-splitting them later anyway.

How to do it

Most important first.

  • Take the child, say what it does as a sequence of steps a person could watch, and make each step a node. Sequences are the natural shape for workflows; capabilities are the natural shape for products.
  • For each step, write "works when". Stop recursing wherever the sentence comes easily and the step is buildable (What Makes a Good Subproblem).
  • Write down every decision the split forced. Each one is an unknown for the parent, with a specific question and an experiment (Unknown, Question, Experiment).
  • Do not decompose siblings ahead of need; note "one level for now" on them and move on.

Worked on a concrete problem

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

  • Checkout, split: Load Cart (works when: the current customer's lines are returned; an empty cart is refused). Validate Items (works when: an out-of-stock line is reported by name; all-valid passes). Calculate Total (works when: total equals the sum of quantity times current price). Create Payment (works when: a test-mode charge is created for the total and its id stored). Create Order (works when: exactly one order exists with the validated lines and captured prices). Confirmation (works when: the customer sees the order number and the cart is empty). Six leaves; each passes the leaf test; the recursion stops.
  • Decisions the split surfaced: is the order created before payment (so a failed payment leaves an unpaid order) or after (so a failed payment leaves nothing)? Whose price applies if it changed since add-to-cart? Is stock reserved at validate or decremented at create-order? Three questions that were invisible while checkout was one word, now on the unknowns board with experiments.
  • One leaf that needed another level: Create Payment, on inspection, is "ask the provider for a charge" and "find out whether it succeeded", and the second happens on the provider's schedule, not ours. Two leaves, and the second one is the idempotency problem the whole module keeps meeting (Duplicate Requests).

How you know it worked

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

  • The child that was a sentence is now a sequence of leaves, each with a "works when".
  • A list of decisions exists that did not exist before the split, and each is phrased as a question with an experiment.
  • The siblings you have not reached are still one level deep, on purpose, with a note saying so.

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 does this piece do, as a sequence of steps someone could watch?
  • ?Does this step have a "works when" I can write easily — and if so, why am I still splitting?
  • ?Which decisions did the split force into the open, and have they become questions with experiments?
  • ?Which siblings have I left at one level, and did I note that it was deliberate?

What can go wrong

How the move itself fails
  • Recursing everywhere at once. Six children of checkout, each split into four, each split into three, before a single leaf is built — and the deep leaves under Confirmation encode guesses about a payment flow nobody has run yet.
  • Stopping too early because the sentence feels obvious. "Create Payment" looked like a leaf until someone asked when the provider answers; a leaf whose "works when" hides a second actor is usually two.
  • Losing the surfaced decisions. The recursion asked "order before or after payment?" and the answer was made silently in code, so nobody can later say why the order exists unpaid.
What the move costs
  • Local recursion means the tree is uneven — one branch to leaves, others shallow — which looks unfinished to anyone expecting a complete plan.
  • Surfacing decisions early means answering them early, and some of them (order before or after payment) would have been easier to answer with more of the system built.
Misreads
  • "Recurse until leaves are one function." The stopping rule is buildable-and-testable, and a leaf can be several functions. A tree that mirrors the call graph has recursed past the problem into the code.
  • "The steps must be sequential." Checkout happens to be a sequence; a capability like "browse" splits into parallel things (list, view, filter). The recursion is the same; the shape of the children follows the piece.

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.

  • GENERALSplitting a child by the same question that split its parent, and stopping at buildable leaves, is the same move for a workflow, a compiler pass or a data pipeline; only the shape of the children (sequence, parallel set, alternatives) changes.
  • CONTESTEDSome practitioners prefer to decompose the whole tree to leaves before building anything, arguing that the surfaced decisions interact — order-before-payment affects inventory reservation affects confirmation — and that discovering them branch by branch means re-deciding earlier branches. Their strongest point: the decisions the recursion surfaces are exactly the ones that span branches, so a local recursion sees each one from one side only. The reply here is that a shallow full tree plus deep local recursion catches most cross-branch decisions at the shallow level, while a full deep tree encodes guesses.
  • ILLUSTRATIVECheckout's six steps and the three decisions they surface are invented for the example; a store with reservations, coupons or split shipments would surface different ones.

Where the depth lives

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