Decomposing Checkout
The whole module applied to one capability: checkout from a sentence to testable leaves, with the layer it crosses, the decisions the split surfaces, the dependency it cannot control, and the tool it did not need.
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.
Can you take the hardest capability in the store from "turn a cart into a paid order" all the way down to leaves you could build and check — without the decomposition becoming a design document?
Every other capability in the store has been built or stubbed. Checkout is the one that makes it a store, it touches everything, and it is the one you have been putting off. Someone has already suggested that checkout "needs a saga" and someone else has said "just call the provider and save the order".
Look up how checkout is done. Find an article, a reference architecture, a diagram with an orchestrator and compensating transactions, and adapt it. It is the most-solved problem in commerce; surely the shape is known.
The reference architecture is for a store with warehouses, fraud checks, split shipments and three payment providers; every box in it is a decision your store has not needed to make. Adapting it means deleting, and you cannot tell which deletions are safe because you never derived the boxes.
- The reference architecture is for a store with warehouses, fraud checks, split shipments and three payment providers; every box in it is a decision your store has not needed to make. Adapting it means deleting, and you cannot tell which deletions are safe because you never derived the boxes.
- The vocabulary — saga, orchestrator, compensation — arrives before the requirement it serves. Whether your checkout needs any of it depends on whether two systems can disagree about an order, and nobody has asked whether they can.
- The article's happy path becomes the plan, its failure handling becomes "phase two", and phase two is where all the actual difficulty of checkout lives.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Run the module in order, on this one capability. Capability: checkout is a sentence with an actor and a verb (Decomposition by Capability). Recurse: the sentence becomes six observable steps (Recursive Decomposition). Leaf test each: understandable, testable, meaningful, small (What Makes a Good Subproblem). Map the edges and mark which must answer before the customer is told (The Dependency Map). Check the shape (Recognising a Bad Decomposition).
- Then do what the reference architecture skipped: write down the decisions the split forced, and for the tool everyone proposed, climb the why ladder from the claim to the requirement before accepting or rejecting it (The Why Ladder).
- Finish with the first slice: the thinnest path through the leaves that a customer could complete, with the external edge stubbed, so checkout works end to end before the hardest leaf is real (The Walking Skeleton).
Checkout, to the leaves
The full tree, including the failure leaves that the reference architecture deferred to "phase two". They sit under the steps they belong to, each with the observation that would show it works. Nothing here names a layer, a tool or a pattern.
- ├Load cart
- └Current linestestable The customer's lines and quantities are returned with current prices.
- └Empty carttestable Checkout is refused with a message, and no order is created.
- ├Validate items
- └Out of stocktestable The line is reported by product name and the customer can remove it; nothing is charged.
- └Price changed since addtestable The new price is used, and the confirmation page names the change.
- └Calculate totaltestable Total equals the sum of quantity times current price, and is never negative.
- └Create order (pending)testable Exactly one order with the validated lines and captured prices exists in pending; a second submit of the same cart does not create a second one.
- ├Create payment
- └Request a chargetestable A test-mode charge for the total is created with the order id as the idempotency key and its id stored on the order.
- └Learn the outcometestable A confirmation moves the order to paid once; a duplicate confirmation changes nothing; a decline moves it to failed with a reason.
- └No answertestable After a timeout the order stays pending, the customer is told the outcome is unknown, and a later confirmation still lands correctly.
- └Decrement stocktestable On paid, each product's stock is lower by the ordered quantity and never below zero, even with two orders racing for the last unit.
- ├Confirmation
- └Paidtestable The customer sees the order number and amount; the cart is empty.
- └Failedtestable The customer sees the reason and can retry; the cart is intact; retrying reuses the same pending order.
Refunds, partial shipment and coupons are absent because V1 does not have them; each is written beside the tree with the reason.
"We need a saga", laddered
The tool that was proposed before the requirement was known. The ladder does not say sagas are bad; it says what this store actually needs and what meets it — and names the store in which the saga would have been right.
“Checkout needs a saga with compensating transactions.”
- ↓Why a saga? Because creating the order and taking the payment can disagree — one succeeds and the other fails.
- ↓Why is disagreement a problem? Because an order could exist that was never paid, or a charge could exist for an order that does not.
- ↓Why can they disagree? Because the provider is outside our system and answers on its own schedule; our database and the provider are two sources of truth.
- ↓Why not one writer? They cannot be one writer — but our side can be: the order row is the single place that records the outcome, and the provider's confirmation updates it.
the claim was right when Two of our own services each own part of the outcome — an orders service and a separate inventory or ledger service — and neither can be the single writer. Then the disagreement is inside our system and a saga (or an outbox and a reconciler) is the honest answer (Sagas: Trading Isolation for Availability in Distributed Systems).
One order for building it
The leaves, the edges and the ladder decide the order below. The slice is first because it proves the leaves connect; the spike runs beside it because the external edge is on the critical path; the failure leaves follow in the order of how badly they would hurt a customer.
- 1The slice: load → validate → total → pending order → fake payment → paid → confirmation
because Proves every layer connects and every internal leaf works, with the one uncontrolled edge stubbed.
- 2Payment spike in parallel: request a charge, receive a confirmation, log both
because Answers "who says it is paid, and how does that reach us?" before the code depends on the answer.
- 3Replace the fake: request a charge, learn the outcome, idempotently
because The spike is understood; now it becomes the leaf, with the duplicate-confirmation test from the tree.
- 4Decline and no-answer
because The two ways a customer is left not knowing whether they bought something; the pending state was designed for exactly these.
- 5Stock decrement with the race
because Depends on paid existing; the invariant "never below zero" is tested with two orders for the last unit.
- 6Empty cart, price change, double submit
because Each is a leaf with a test; none blocks a customer from buying, so they come after the ones that do.
How to do it
Most important first.
- Write the capability as a sentence and split it into the steps a customer could watch happening. Six is typical for checkout; the number is not the point.
- For each step, write "works when" and check the four criteria. Split the ones that hide a second actor.
- Draw the edges. Mark the external one. Decide per edge whether the customer waits on it.
- List the decisions the split surfaced. Put each on the unknowns board with an experiment; do not answer them in code.
- For any tool someone has proposed, ladder it: why → why → the real requirement → the simpler thing → the case where the tool was right.
- Choose the first slice and build it against a stub; replace the stub when the spike lands.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The sentence: "a customer turns their cart into a paid order and sees a confirmation." Steps: load cart, validate items, calculate total, create payment (two leaves: request charge, learn outcome), create order, confirmation. Every leaf has a "works when"; the tree passes the shape check — six children, deepest leaf at level two, no layer names, no vague names.
- Edges: cart, inventory, orders are internal and before-responding; the provider is external and before-responding; email is external and eventually. Critical path has one edge we do not control, so the payment spike is scheduled before Checkout is built against the real thing.
- The proposed saga, laddered: "we need a saga" → why → because order creation and payment can disagree → why is that a problem → because an order might exist unpaid or a payment exist without an order → what do we actually need → every order to end in exactly one of paid, failed or cancelled, and every charge to belong to an order that ends paid. The simpler thing: create the order first in pending, request the charge with the order id as the idempotency key, and let the provider's confirmation move it to paid — one process, one database, no compensation. Justified when: there are two of our own services that each own part of the outcome, and neither can be the single writer.
- First slice: product page → add to cart → checkout page → create pending order → fake payment succeeds → order paid → confirmation shows the order number. It is thin, crosses every layer, and proves the leaves connect. It does not prove the provider behaves, that a decline is handled, or that two checkouts of the last unit are safe — and those are the next three leaves.
How you know it worked
What now exists that did not before, and what question you can now ask.
- A tree, a board and a map exist for checkout, and they fit on a page.
- The proposed tool has been either rejected with the requirement it did not serve, or accepted with the requirement it does.
- A customer can complete checkout against a stub, and you can say precisely what the stub hides.
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.
- ?What are the observable steps of this capability, and does each have a "works when"?
- ?Which decisions did splitting it force, and where are they written down as questions?
- ?For the tool someone proposed, what is the requirement it serves, and is there a simpler thing that serves the same one?
- ?What is the thinnest path through the leaves a customer could complete, and what does completing it not prove?
What can go wrong
- Decomposing checkout into the reference architecture's boxes and calling it your own. The tree looks derived; it was copied, and the copied boxes carry requirements you do not have.
- Answering the surfaced decisions inside the leaves — "create order" quietly decides order-before-payment — so the board is empty and the decision is invisible.
- The slice that proves everything: the fake payment becomes the payment, the decline path is never built, and the store ships handling only success (What If Payment Fails?).
- Deriving checkout from scratch costs more than adapting a reference, and on a store that genuinely matches the reference — multiple providers, split shipments — the derivation arrives at the same boxes more slowly.
- Rejecting the saga now means revisiting the decision if a second service ever owns part of the outcome; the decision journal should say what would trigger that (The Decision Journal).
- "So sagas are over-engineering." The ladder said the saga was justified when two services each own part of the outcome. It rejected the saga for *this* store, on the evidence that there is one writer; the same ladder accepts it elsewhere.
- "The slice is checkout." The slice is checkout's happy path against a stub. It is the first thing to build and the smallest thing that could be mistaken for done.
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.
- DOMAIN-SPECIFICCheckout in a goods store is a sequence with one external dependency; checkout for a subscription, a marketplace with two sellers, or a store with reservations has different steps and more than one writer, and the saga the ladder rejected here may be the right answer there.
- CONTESTEDSome practitioners hold that the pending-order-first design chosen here is wrong, because it creates orders the customer never intended to complete and forces cleanup of abandoned pending orders; they prefer to create the order only on payment success, accepting that a confirmation which arrives after a crash must be reconciled against the cart. Their strongest point: the pending-order design turns every abandoned checkout into a record that support will one day ask about. The reply is that a pending row with a status is easier to reason about than a charge with no order, and abandonment is a query, not a bug.
- ILLUSTRATIVEThe six steps, the ladder, the slice and the pending-order design are invented for the running example; a real provider's flow and a real store's policies would change the details.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — Read one of the delegation cards at /manifesto/delegating for a payment SDK before the spike: it lists what the SDK handles and what — idempotency, the order row, the timeout — stays yours.