PseudocodeGENERALCONTESTEDILLUSTRATIVE

Pseudocode Before Code

"function checkout(cart): validate cart, calculate total, create payment, create order, return confirmation" — five lines that say what checkout does, written before any framework decides how. The lines are where the missing decisions become visible.

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 understand the problem and are about to write checkout. What do you write first, and why not the code?

The situation

I know what checkout has to do — I did the requirements, I have the decomposition. I opened the handler file, typed the route, started the request parsing, and half an hour later I am reading the framework's validation docs and I have not written the part where a payment happens. I keep finding the next framework question instead of the next checkout question.

The reflex

Start typing in the real language, because the design is in your head and the code is where it has to end up anyway. Writing it "twice" — once as pseudocode and once for real — feels like the kind of ceremony that slows people down.

Why it stalls

The framework asks its questions first and they crowd out yours. How is the body parsed, how does validation report errors, what does the ORM call the transaction — each is real and each is answered before "what is the total when a price changed since the cart was filled?", which is the question the store actually needs answered.

What the reflex produces — and fails to produce
  • The framework asks its questions first and they crowd out yours. How is the body parsed, how does validation report errors, what does the ORM call the transaction — each is real and each is answered before "what is the total when a price changed since the cart was filled?", which is the question the store actually needs answered.
  • The syntax hides the branches. A handler that reads as one straight-line function has an implicit branch at every call that can fail, and in the framework's idiom those branches are exceptions caught somewhere else. On the page, checkout looks like it has one path; it has six, and the five you cannot see are the failure model.
  • What was in your head is in the code now, and nowhere else — so the only way to review the design is to review the implementation, and the only person who can do that is someone who reads this framework.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Write the operation as a short function in no language: its name, what it takes, what it returns, and the steps in between as plain verbs. "function checkout(cart): validate cart; calculate total; create payment; create order; return confirmation." The step names are the decomposition and the parameters are the interface, and nothing here can be wrong about a framework because no framework appears (Decomposing Checkout, Which Components Must Communicate?).
  • At each step, ask what it needs that is not yet in the function and what it does when it cannot proceed. "Calculate total" needs prices — from the cart or from the catalog now? "Create payment" can fail — then what happens to the order? Each question is written into the pseudocode as a branch or a parameter, and each is a decision that would otherwise have been made silently by whichever framework call happened to be nearest (Pseudocode as a Thinking Tool).
  • Stop when every step is either something you know how to write in any language or a named unknown with a question attached. That is the point at which the framework question has content: you are now choosing a tool for a known shape, not discovering the shape through the tool (Problem Before Technology).

Five lines, then the questions they raise

The first version is deliberately naive. It is the happy path as verbs, and its value is that reading it back raises the questions the framework would have answered for you. The second version below it is the same function after those questions were asked.

Checkout, first pass and second pass
1# first pass — the happy path, as verbs
2function checkout(cart):
3 validate cart
4 calculate total
5 create payment
6 create order
7 return confirmation
8
9# second pass — after asking "from what?" and "and if not?"
10function checkout(cart, attemptKey):
11 if cart is empty: return rejected("empty cart")
12 prices = current prices for cart items # catalog now, not cart then
13 if any price differs from cart: return rejected("price changed", differences)
14 if any item lacks stock: return rejected("out of stock", item)
15 total = sum(price × quantity)
16 order = create order(pending, cart, prices, total)
17 payment = provider.charge(total, attemptKey)
18 if payment declined: mark order failed(reason); return failed(reason)
19 if payment unknown: ??? # timed out — not decided yet; unknowns board
20 mark order paid(payment)
21 return confirmation(order)

Everything that changed between the passes is a decision: where prices come from, that a price change stops checkout, that the order exists before the charge, and that the timeout case is not yet decided. None of those decisions is about a framework.

What the framework-first version hides

The comparison is the same operation begun in a framework's idiom. It is not worse code; it is code in which the decisions above were made by default — the total from the cart's stored prices, failures as exceptions caught elsewhere, the order written after the charge because that was the order the lines came out in.

Begun in the framework
A route handler that parses the body with the framework's validator, loads the cart with the ORM, sums cart.items[].price, calls the payment SDK inside a try block, saves an Order in the catch-less path, and returns 200. Reads as one path; has six.
Begun as pseudocode, then translated
The second pass above, with each branch then mapped to the framework: rejected → a 4xx with a body naming the reason; pending order → a row written before the SDK call; declined → an explicit state, not an exception; unknown → a named branch pointing at the unknowns board.

The branches were found by reading verbs, which is cheap, instead of by a customer hitting a default, which is not. The framework still does all the work; it just does it for decisions that were made on purpose (Framework Independence).

One order of work, and the case for another

The sequence below is the one the module recommends: pseudocode, then the tests the branches imply, then the code. The slogan "write tests first" becomes precise here — the tests come from the branches, and the branches come from the pseudocode, so test-first without the pseudocode is testing the paths you happened to think of.

From verbs to a running checkout
  1. 1
    Happy-path pseudocode: name, inputs, output, steps as verbs

    because Cheapest possible statement of what the operation is; a screenful; no tooling.

  2. 2
    Second pass: "from what?" and "and if not?" at every step

    because Surfaces the decisions; each branch becomes a state or a named unknown.

  3. 3
    One test per ending — rejected, failed, paid, unknown

    because The branches are the test list; writing them now pins the design before the framework reshapes it.

  4. 4
    Translate to code, keeping each branch visible

    because The framework now serves a known shape, and the tests say whether the translation kept the branches.

a different valid order Sketch in the real language with stubs: write the function in your language with every hard step as a stub that returns a fixed value, and grow the branches in place. You would choose this when the language's types catch the mistakes pseudocode cannot and the team reads the language fluently — the condition being that the branches stay on the page rather than in a catch block.

How to do it

Most important first.

  • Name the function and its inputs and output first. If you cannot name the output, you do not yet know what the operation is for.
  • Write the steps as verbs on their own lines, in order, without conditionals. This is the happy path and it should fit on a screen.
  • Go back over each line and add the branch for "cannot": empty cart, price changed, payment declined, order write fails. Every added branch is a requirement you had not written down (Failure Path Second).
  • Mark anything you do not know how to do with a question, not a guess, and take the question to the unknowns board (Unknown to Specific Question).
  • Only now open the framework docs, and open them for the specific step — "how does this framework do a transaction across these two writes?" — not for "how to build checkout".

Worked on a concrete problem

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

  • First pass, happy path only: function checkout(cart): validate cart; calculate total; create payment; create order; return confirmation. Reading it back: "calculate total" from what prices? Decision: from the catalog at checkout time, not from the cart, so that a price change is caught — which means "validate cart" must also compare, and there is a new branch: price changed → tell the customer, do not proceed.
  • Second pass, with the "cannot" branches: empty cart → reject before anything else; item out of stock → reject naming the item; payment declined → order stays unpaid, return failure with the provider's reason; payment timed out → this branch had no answer, and it became the first unknown on the board rather than a guess in the code.
  • The framework question at the end: something that can parse a request, run two writes in one transaction, and call an HTTP API with a timeout. Every framework the team knows can do all three; the choice took a minute because the pseudocode had already said what was needed.

How you know it worked

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

  • A screen of plain-language steps exists that a colleague who does not know your framework could read and argue with.
  • The branches are visible on the page — you can count the ways checkout ends, and each ending names a state.
  • The first framework question you asked was about a specific step, and you could say why that step needed 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 name, the input and the output of the operation I am about to write?
  • ?What are the steps, as verbs, on the happy path — and does each step have what it needs?
  • ?At each step, what does the operation do when it cannot proceed, and what state does that leave?
  • ?Which step do I not know how to write in any language — and what is the question hiding in it?

What can go wrong

How the move itself fails
  • The pseudocode grows a type system, error codes and a module structure, and becomes a second implementation in a language that does not run. Its value is that it is cheap; when it stops being cheap, write the real thing.
  • It is written once and never read again. The branches that made it useful have to survive into the code, or the pseudocode was a warm-up rather than a design (From Pseudocode to Code).
  • It is written for operations that have no branches — a lookup, a rename — where the real code is the same length and clearer. Pseudocode earns its place where there are decisions to make.
What the move costs
  • It is written twice. For an operation with real branches the second writing is faster than the first would have been; for a straight-line operation it is pure overhead.
  • Pseudocode has no compiler, so its mistakes — a step that uses a value nothing produced — are found by reading, and only if someone reads it.
Misreads
  • "Pseudocode is for people who do not know the language yet." It is most useful to people who know the language well, because they are the ones the framework's idiom carries away fastest.
  • "Write pseudocode for everything." Write it for operations with branches, state and boundaries. A getter does not need it, and a lesson that says otherwise is prescribing ceremony.
  • "If the pseudocode is good, the code is a transcription." The code adds what the language and the framework add — types, transactions, timeouts — and each addition is a decision. The pseudocode is the design; the code is the design plus the engineering.

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.

  • GENERALNaming the input, output and steps before choosing syntax applies to any operation with decisions in it, from a checkout handler to a compiler pass. The length of the pseudocode scales with the number of branches, not the size of the system.
  • CONTESTEDMany strong engineers hold that pseudocode is a detour: a language with good types and a fast test loop is itself the cheapest place to think, the compiler finds the mistakes pseudocode cannot, and a sketch in a real language can be run. The strongest form: in a language you know well, "pseudocode" is just code with the hard parts stubbed, and writing it in prose first is writing it twice. The lesson's answer is that the detour is worth it exactly when the framework, not the language, would otherwise decide your branches — and that a stub-first sketch in the real language counts, if the branches are on the page.
  • ILLUSTRATIVEThe half hour in the validation docs and the two-pass checkout are invented to show where the decisions surface; a real checkout has more branches than the example lists.

Where the depth lives

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

Further
  • Build Without AI at /thinking/without-ai asks for your pseudocode before any hint, for this reason: the pseudocode is the part of the solution that is yours.