RequirementsCONTESTEDDOMAIN-SPECIFICILLUSTRATIVE

Happy Path First

Build the sequence where everything goes right — cart, pay, order, confirmation — before any failure handling, because the happy path is the thing the failures are failures *of*. The order is contested: the failure-first camp has a real argument, and this lesson says where each wins.

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

Should the first working version of checkout handle payment failure, duplicate clicks and crashes — or should it work end to end with everything going right, and grow failure handling afterwards?

The situation

You have the discovered requirements: no double charge, declined payments explained, crash recovery, out-of-stock before charge. You sit down to build checkout and try to write the first function with all of them in it. The function has nine branches before it has created an order, and you cannot test any of them because none of the surrounding pieces exist yet.

The reflex

Build it right the first time. Every discovered failure goes into the first version: idempotency keys, retries with backoff, a reconciliation job, a state machine for payment status. Leaving them out feels like building something you know to be wrong.

Why it stalls

Nothing runs for a long time. The happy path is buried inside the failure branches, so the first end-to-end test — a customer actually buying something — is weeks away, and every assumption about the provider, the cart and the order stays untested until then.

What the reflex produces — and fails to produce
  • Nothing runs for a long time. The happy path is buried inside the failure branches, so the first end-to-end test — a customer actually buying something — is weeks away, and every assumption about the provider, the cart and the order stays untested until then.
  • Failure handling is built against imagined failures. Without a working charge to break, the retry logic handles the timeout the author pictured, not the one the provider actually produces; the reconciliation job reconciles against an order shape that changes twice before it is real.
  • The nine branches cannot be tested independently, so they are tested together or not at all. A bug in the decline branch hides behind a bug in the timeout branch.
  • Motion looks like progress: a state machine diagram, an idempotency table, a retry policy — and no order has ever been created.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Build the straight line first: the sequence of steps where every external system answers correctly and the user does the expected thing exactly once. Make it work end to end through every layer. Not because failures do not matter, but because every failure is defined relative to a step on the happy path — "payment fails" is meaningless until there is a payment — and a working line gives each failure a specific place to be injected and observed.
  • Keep the happy path honest about what it is not. A happy path that silently swallows errors is a lie; a happy path that stops at the first unexpected condition with a clear failure is a truthful skeleton. The rule is: no failure *handling* yet, but every failure *visible* — an exception, a log line, a status — so that when the failure branches arrive, they replace a loud gap rather than a quiet one.
  • Then add failures in order of cost — the ones that lose money or data first, the ones that inconvenience a customer second, the ones that are merely ugly last — each one injected against the working path and observed (Failure Path Second).
  • Know when to invert the order. If a failure mode could change the *shape* of the happy path — the provider forces a hosted page, or the only way to be safe against double charge is a checkout id created before Pay is clicked — then that failure has to be understood first, because building the happy path without it means rebuilding it. That is the failure-first camp's real argument, and it is right whenever the failure is structural.

The straight line, and where it is not the only order

The order below is one defensible sequence for checkout. Each step says why it comes where it does; the alternative at the bottom is the failure-first order and the condition under which it is the better one. The device says in data what the lesson says in prose: this is a sequence, not the sequence.

Checkout, happy path first
  1. 1
    Structural check: which failures change the shape?

    because A checkout id before Pay, a client id on every message — these are part of the path, not branches off it, and cost a rebuild if found later.

  2. 2
    The straight line through every layer, with loud gaps

    because Every failure is defined relative to a step; until the steps exist there is nothing to inject against, and every assumption about the provider stays a guess.

  3. 3
    Failures that lose money or data: double charge, crash after charge

    because Highest cost, and each is now a specific place in working code with a test that can be written before the branch.

  4. 4
    Failures that sell what we lack: stock at Pay time

    because Costs a customer an apology and a refund; cheaper than the money ones, dearer than the cosmetic ones.

  5. 5
    Failures that inconvenience: declined card explained, refresh mid-payment

    because A customer sees something ugly but nothing is lost; last among the V1 failures.

a different valid order Failure-first: write the payment state machine and the idempotency contract before any page exists, then build the happy path as one transition through them. Choose this when the team already knows the provider's failure modes precisely — from a previous integration or a spike — so that the failures are not imagined, and when most of the discovered failures are structural rather than branches.

A loud gap against a quiet one

What makes happy-path-first safe rather than reckless is the gap policy. The two snippets do the same thing on the happy path and differ entirely on the day the provider declines a card.

Leaving the decline branch for later
Quiet gap
try { charge(checkout) } catch { /* handle later */ }
createOrder(checkout)
Loud gap
const result = charge(checkout)
if (result.status !== 'succeeded') {
  throw new NotHandledYet('declined-payment-explained', result)
}
createOrder(checkout)

The quiet version creates an order for a declined payment and nobody finds out until reconciliation. The loud version fails the checkout, names the requirement it is waiting for, and carries the provider's real response — which is exactly what the failure branch will need when it is written against this spot.

When to invert

The decision is the whole disagreement between the two camps reduced to a question you can ask of each discovered failure. Neither option is "the right way"; the criteria are what decide.

For each discovered failure

Does handling this failure properly change the shape of the happy path?

Structural — understand it first

when The safe handling needs something the happy path must create earlier (a checkout id, a client message id), changes the external contract, or decides which system is authoritative.

cost A spike before any page exists, and a happy path that is slightly more complex from the start; but no rebuild.

Branch — build it against the working path

when Handling it is a branch off an existing step with no change to the sequence or the data — a declined card, a refresh, an out-of-stock message.

cost Touching working code once per failure, and a window where the gap is loud but open; the gain is handling the real failure, observed, not the imagined one.

Deferred with a reason

when The failure was discovered but is not V1 — partial refunds, provider-side disputes — and the loud gap is acceptable at launch.

cost A written reason and a trigger; the gap stays in the code with the requirement's name on it.

How to do it

Most important first.

  • Write the happy path as numbered steps and build exactly those, through every layer, with the test-mode provider and one product (The Walking Skeleton).
  • At each step where a failure was discovered, leave a visible gap: throw, log, or return a status that says "not handled yet". Never a silent catch.
  • Before building, scan the discovered failures for structural ones — anything whose answer changes the sequence, the data model or the external contract. Investigate those first with a spike, not with a full implementation (Spikes).
  • Once the path runs, list the failures by cost and add them one at a time, each against the working path, each with the observation that shows it is handled.
  • Resist adding a failure branch "while you are in there". Each branch is a change to something that works; make it deliberately.

Worked on a concrete problem

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

  • Checkout, happy path: cart → click Pay → charge in test mode → order created paid → stock reduced → confirmation. Built through page, API, logic and database in a short session with a card that always succeeds. Gaps left loud: a declined card throws and the page shows a raw error; a second click would create a second order and a comment says so; a crash between charge and insert loses the order and a log line marks the spot. The path works and every discovered failure has an address.
  • Structural check before building: "no double charge" — does it change the shape? Yes. Preventing it cleanly needs a checkout id that exists before Pay is clicked, so the request can be idempotent on it. That is a change to the happy path itself — the page must create the checkout before rendering the button — so it went in first. "Declined payment explained" — changes nothing structural; it is a branch. Deferred to the failure pass.
  • Failures added in cost order: double charge (loses money) via the checkout id; crash after charge (loses an order) via the provider's confirmation callback creating the order if missing; out-of-stock at Pay (sells what we do not have) via the stock check before the charge; declined card explained (inconveniences a customer) last. Each one was injected against a path that already worked, and each had a test before the branch existed.
  • The chat app, where the order flips: "send message" has a happy path of a few lines, but the structural failure — the client retries and the message must not duplicate — decides whether messages carry client-generated ids. That is failure-first, and building send without it would mean changing every client afterwards. The failure was understood first, the id went into the happy path, and the rest followed the usual order.

How you know it worked

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

  • An order has been created end to end before any failure branch exists, and the date it first happened is early in the project.
  • Every discovered failure has a visible gap in the working code — a throw, a log line, a comment with the requirement's name — rather than a silent one.
  • Structural failures were identified before building and either spiked or built into the path; the rest arrived one at a time afterwards.
  • Each failure branch was tested by injecting the failure against the working path, not by imagining it.

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 straight line through this feature where everything answers correctly once — and can I build only that, through every layer?
  • ?Which of the discovered failures would change the shape of the happy path if handled properly — and have I understood those before building?
  • ?Where are my loud gaps — and is any of them quiet?
  • ?In what order do the failures cost the most, and which one gets injected against the working path first?

What can go wrong

How the move itself fails
  • The happy path ships. It was meant as the first step and became the product because it demoed well; the loud gaps became quiet ones under a catch-all error page; the first double click created two orders in production.
  • Failures are silently swallowed to keep the happy path "clean". A catch that logs nothing is not a happy path, it is a hidden failure path, and it will be found by a customer.
  • The structural check is skipped, and the happy path is built in a shape that cannot be made safe — the charge happens before any checkout record exists, and adding idempotency later means rebuilding the sequence.
  • The move is applied to a domain where the failure path is the product — a payments processor, a backup tool — and the "happy path" is the least interesting part of what is being built.
What the move costs
  • A working happy path invites the demo, and the demo invites the question "so it's done?" — the loud gaps are the honest answer, and they have to be shown.
  • Building failures afterwards means touching working code again for each one; a failure-first build touches it once. Where the failures are well understood and structural, that is the cheaper order.
  • The happy path with loud gaps is briefly a system that loses money on a double click. It must not see a real customer in that state, and someone has to make sure it does not.
Misreads
  • "Happy path first means failures come later, so they can be cut." The order is about *building*, not about scope. Every discovered V1 failure is still V1; the sequence just puts the thing they are failures *of* first.
  • "The happy path is the MVP." It is the first slice of it. An MVP checkout that double-charges on a double click is not minimal, it is broken — see MVP vs Bad Prototype.
  • "Failure-first is over-engineering." Failure-first for structural failures is the only way to avoid rebuilding; failure-first for every failure is the reflex this lesson opens with. The line is whether the failure changes the shape.

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.

  • CONTESTEDThe failure-first camp argues that the happy path is the easy part and encodes assumptions that failure handling then has to undo: a charge made before a checkout record exists cannot be made idempotent without restructuring, a message without a client id cannot be deduplicated without changing every client, and a team that builds the happy path first ships it because it demos well. On their view you design the failure semantics — idempotency, state transitions, what is authoritative — before writing the success case, and the success case falls out of them. That view is right whenever a failure is structural, and this lesson's "structural check first" is the concession; it is wrong for the long tail of branch-shaped failures, where building against imagined failures produces handling for the wrong ones.
  • DOMAIN-SPECIFICFor a store or a chat app the happy path is most of the product and the order holds; for a payments processor, a backup system or a consensus protocol the failure semantics are the product, and the "happy path" is a degenerate case built last.
  • ILLUSTRATIVEThe checkout session, the four failure branches and the cost ordering are invented to show the sequence; a real checkout has its own list and its own structural failures.

Where the depth lives

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

Further
  • The slice visualizer at /thinking/slices shows the same straight line through frontend, API, logic and database, with what it proves and does not.