PseudocodeGENERALSIMPLIFIEDILLUSTRATIVE

Inputs, Outputs, State, Branches

The four things every operation has and pseudocode must show — what comes in, what goes out, what is remembered, where the path splits — plus failures, which are branches with consequences. Checkout under all five, and the chat app for contrast.

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

What must a piece of pseudocode contain before it can be said to describe an operation, and how do you check it does?

The situation

My pseudocode for checkout looks complete but I cannot tell whether it is. Two people read it and asked different questions — one about where the prices came from, one about what happens after a decline — and both questions should have been answered on the page. I need a check I can run on my own pseudocode before someone else does.

The reflex

Add more detail everywhere, so that nobody can ask a question the page does not answer. Longer pseudocode feels safer, and it is easier to add lines than to know which lines are missing.

Why it stalls

Detail added evenly buries the gaps rather than filling them. The pseudocode grows to two screens, the reviewer still asks about prices, and now the answer is harder to find because it is missing among more lines instead of among fewer.

What the reflex produces — and fails to produce
  • Detail added evenly buries the gaps rather than filling them. The pseudocode grows to two screens, the reviewer still asks about prices, and now the answer is harder to find because it is missing among more lines instead of among fewer.
  • Without a check, "complete" means "I stopped". The two reviewers each found a hole by luck — the hole that matched what they happened to worry about — and the holes neither of them worried about are still there.
  • The detail that got added was the kind that felt like progress: variable names, a loop, the shape of the confirmation object. None of it was a branch or a piece of state, which is where the questions came from.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Check the pseudocode against five headings, in order: inputs — everything the operation needs, with where it comes from; outputs — every way it can end, each with what it returns; state — everything it reads or changes that outlives the call, with who else touches it; branches — every point where the path splits, with the condition; failures — every branch whose other side leaves something changed. An operation is described when all five are answerable from the page (Pseudocode as a Thinking Tool).
  • The check is a reading, not a rewrite. Read the pseudocode once per heading and, for each, write the answer beside it. Where the answer is "not on the page", that is the missing line — and it is usually one line, in a specific place, not more detail everywhere (Overwrite or Append?).
  • Treat outputs as endings, not return types. Checkout ends in confirmed, rejected, failed and unknown, and each ending is a state the order is in. Listing the endings is how you find the branch you forgot, because a branch you forgot has an ending you have not named (The Order Lifecycle, Built).
  • Treat failures as the branches with a side. A rejected cart changes nothing; a declined payment leaves an order in failed; a timed-out payment leaves one in unknown. The failures heading asks, for each branch, what is different afterwards — which is the question the failure model asks of the same operation from the other direction (Failure Modeling).

Checkout, under the five headings

The matrix is the answer sheet for the second-pass checkout from Pseudocode Before Code. The cells marked as missing are what the check found; each became one line in the pseudocode, in a specific place. The check did not make the pseudocode longer everywhere; it made it complete in four places.

HeadingAnswer from the pageWas missing
Inputscart, attempt key, current prices, current stockSource of cart (session or account); when the attempt key is generated
Outputs (endings)confirmed, rejected(reason), failed(reason)unknown(order) — the timeout ending
Statereads catalog and stock; writes order, reservation, attempt keyThat the reservation is written by every concurrent checkout
Branchesempty, price changed, out of stock, declinedtimed out; order write fails after charge
Failures (what is left changed)declined → order failed, reservation releasedtimed out → order unknown, reservation held; write-after-charge → charge with no order

The chat app, for contrast

The same check on a different operation, to show that the headings are constant and the findings are not. In checkout the failures heading found the most; in "send message" the state heading did, because messaging is mostly about what several people remember about the same conversation.

Send message, after the check
1function sendMessage(conversationId, senderId, body, clientMessageId):
2 # inputs: clientMessageId comes from the client, generated once per attempt
3 if sender not a member of conversation: return rejected("not a member")
4 if message with clientMessageId exists: return sent(existing) # a repeat
5 # state: the sender's sequence number is read and incremented here,
6 # and read by every client that orders messages — its owner is the server
7 seq = next sequence for (conversationId, senderId)
8 message = store(conversationId, senderId, seq, body, clientMessageId)
9 deliver to members # may be slow; the send does not wait
10 return sent(message)
11
12# endings: sent, rejected. "queued" is the client's ending, not the server's —
13# the client keeps the message locally with clientMessageId and retries.
14# failures: a queued message later rejected must be shown as failed, not dropped.

Two of the four hardest parts of a chat app — the client message id and the sender sequence — appear here as a consequence of the inputs and state headings. Neither was in the first draft.

Endings as a decomposition

The outputs heading gives a decomposition for free: one leaf per ending, each testable by producing that ending and observing the state. The tree is the test list for checkout, and it was derived from the pseudocode rather than from imagination.

Checkout, by ending
Checkout reaches every ending correctly
  • Rejected before anything changes
    • Empty carttestable An empty cart returns rejected("empty cart"); no order, reservation or attempt key exists afterwards.
    • Price changedtestable A cart whose stored price differs from the catalog returns rejected with the differences; nothing written.
    • Out of stocktestable A cart with an unavailable item returns rejected naming the item; nothing written.
  • Failed after the order exists
    • Declinedtestable A declined test card leaves the order in failed with the reason and the reservation released.
  • Unknown
    • Timed outtestable A hanging provider leaves the order in unknown with its reference stored and the reservation held.
  • Confirmed
    • Paidtestable A successful charge leaves the order paid, the reservation committed, and returns the confirmation for that order.

Four endings, six leaves, six tests. An ending with no leaf under it is a state the code will reach without a test that puts it there.

How to do it

Most important first.

  • Inputs: list them, and for each write its source. An input with no source is a parameter you will have to invent later — usually a session, an id or a clock.
  • Outputs: list every ending. If the list has one item, the operation has no branches on the page yet, and the next heading will find them.
  • State: list what is read and what is written that outlives the call, and beside each write, who else writes it. Shared writes are where concurrency questions live (Who Owns This State?).
  • Branches: number the split points and check each has both sides written. An "if" with no "else" on an operation with state is a branch whose other side has not been decided.
  • Failures: for each branch, ask what is left changed on the losing side. Write the state, or write "nothing" — both are answers; a blank is not.

Worked on a concrete problem

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

  • Checkout under the five headings. Inputs: cart (from the session or account — which?), attempt key (from the client, generated when?), current prices (from the catalog, now), current stock (from inventory, now). Two of the four had no source on the page, and both became lines. Outputs: confirmed(order), rejected(reason), failed(reason), unknown(order) — the fourth was missing and it is the timeout ending. State: reads catalog and stock; writes the order, the stock reservation and the attempt key; the reservation is also written by every other checkout, which is the concurrency question.
  • Checkout branches, numbered: empty cart; price changed; out of stock; declined; timed out; order write fails. The last two had no other side written. Failures beside each: empty, price, stock → nothing changed; declined → order in failed, reservation released; timed out → order in unknown, reservation held, reference stored; write fails after charge → charge exists, and this is the partial-failure gap that needs a pending row before the charge (Partial Failure).
  • The chat app's "send message" under the same five, for contrast: inputs — conversation, sender, body, client message id; outputs — sent(message), rejected(not a member), queued(offline); state — the conversation's messages, the sender's sequence number, and each recipient's read position, which is read by the send and written by someone else; branches — membership, duplicate id, offline; failures — a queued message that is later rejected must be shown to the sender as failed, not silently dropped. The state heading found the read position; the failures heading found the silent drop.

How you know it worked

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

  • Beside the pseudocode there is a five-line answer sheet — inputs with sources, endings, state with owners, numbered branches, failures with what changed — and every line was answered from the page.
  • The check found at least one missing source, one missing ending, or one branch with no other side; if it found none, read it once more as the state heading, which is the one most often skipped.
  • A reviewer's question can be placed under one of the five headings, and either the page answers it or you know exactly which line is missing.

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 operation need, and where does each thing come from?
  • ?How many ways can it end, and what state does each ending leave?
  • ?What does it read or write that outlives the call, and who else writes that?
  • ?For each branch, what is different afterwards on the losing side?

What can go wrong

How the move itself fails
  • The five headings become a form filled in before the pseudocode is written, and the pseudocode is written to satisfy the form. The check is a reading of something that exists; run it after, not before.
  • State is listed as "the database". The heading wants the specific things read and written and who else writes them; "the database" hides the reservation that every checkout races for.
  • Failures are answered with mechanisms — "rollback", "retry" — instead of with what is left changed. The heading asks about the state after, and "retry" is not a state.
What the move costs
  • Five readings of the same page take longer than one, and on an operation with no state and one ending they find nothing; the check is worth running where the earlier headings already found something.
  • The answer sheet is one more artefact to keep next to the pseudocode, and it goes stale the moment the pseudocode changes.
Misreads
  • "The five headings are a template for writing pseudocode." They are a check for reading it. Pseudocode written heading by heading tends to be a form; pseudocode written as an operation and then checked tends to be a design.
  • "State means the database schema." State here means what outlives the call, wherever it lives — a session, a counter in memory, a provider's record of a charge. The schema is one place state lives, and the heading is asking about all of them.
  • "Failures are a subset of branches, so the fifth heading is redundant." It asks a different question of the same branches — not "where does it split" but "what is left changed" — and that is the question that finds the partial-failure gap.

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.

  • GENERALEvery operation in every system has inputs, outputs, state and branches; the check applies unchanged to a database migration, a UI event handler or a pipeline stage. What differs is which heading finds the most — for a pure function the state heading is empty by design.
  • SIMPLIFIEDFive headings is a teaching model; real operations also have timing (what happens while it runs), concurrency (what happens when two run at once) and observability (what gets logged), which this lesson folds into state and failures rather than naming separately.
  • ILLUSTRATIVEThe two reviewers and the checkout answer sheet are invented; the chat app walkthrough shows the headings finding things and is not a complete design for messaging.

Where the depth lives

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

Further
  • The manifesto's layer view at /manifesto/layers is the same check applied to a whole system: what each layer takes, returns and remembers.