ExamplesGENERALTEAM-SPECIFICILLUSTRATIVE

Predict the Bug

"Every addItem call inserts a new row." Before running it, say what happens when the same product is added twice. Duplicate entries — and the missing line is a rule, not a typo. Reading code for the example it would get wrong is how you review a cart you did not write.

The moveWorked exampleNext questions▶ Cart Lab

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 are given a cart implementation you did not write. How do you find its bug from the examples, before running it — and how do you know the bug is a missing rule rather than a slip?

The situation

A teammate's addItem is three lines: push the item, return the cart. It is clean, it has a test, the test passes. You are asked to review it, and everything you can see is fine. You cannot point to a wrong line, because there is no wrong line — there is a missing one.

The reflex

Read the code for mistakes: off-by-one, wrong variable, missing return. Find none, approve it. Reviewing means looking for errors in what is there.

Why it stalls

The bug is an absence, and reading for presence cannot find it. Every line of the three is correct; the cart is still wrong on the second add.

What the reflex produces — and fails to produce
  • The bug is an absence, and reading for presence cannot find it. Every line of the three is correct; the cart is still wrong on the second add.
  • The test that exists is the normal case — add a laptop, see a laptop — so it passes, and its passing is taken as evidence about a case it never exercised.
  • When the duplicate is eventually reported, it gets fixed as a slip — a de-duplication in the view — instead of as the rule it is, and changeQuantity still cannot tell which of the two entries to change.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Review with examples, not with eyes. Take the three kinds — normal, edge, invalid — for the operation and predict, from the code, what each would produce. The normal case will pass; it always does. The edge case is the one to predict carefully: what does this code do when the product is already there?
  • Trace the edge example through the five stops. Input: laptop, on a cart that has a laptop. Lookup: is there one? In the buggy version there is no lookup stop — and a missing stop is the finding. Branch: none. Mutation: append. Output: two entries. You have predicted the bug without running anything.
  • Name the bug as a rule, not as a line. "The find is missing" is a symptom; "the rule 'one entry per product' is not encoded" is the diagnosis, and it says what the fix has to be — a lookup and a branch, not a de-duplication somewhere downstream.
  • Then run the example to confirm, and write it as the test that was missing. The review's output is not "add a find"; it is the add-again test, failing, with the rule named — after which the fix is obvious and its correctness checkable.

The code, and the example to predict

The buggy version from the concept record, and the question the record attaches to it. Read it once for slips — there are none — and then for the edge example: what happens when the same product is added twice? The answer is predictable from the code because the code has no lookup, and the prediction is the review.

Predicted from the code, before running
before
Cart = [ Laptop × 1 ]
add Laptop →
after
Cart = [ Laptop × 1, Laptop × 1 ]
what changed An entry appeared — where the example says a quantity should have changed · Rule violated: one logical entry per product · Downstream: the view shows a duplicate; the total is right by accident; changeQuantity has two entries and no way to choose
A version of addItem that skips the find and always appends
1function addItem(cart, productId, quantity = 1) {
2 cart.items.push({ productId, quantity })
3 return cart
4}

Nothing here is wrong. Something is missing. Reading for errors in what is present cannot find it; predicting the edge example can.

The trace with a missing stop

Tracing the edge example through the five stops makes the omission visible as an empty stop. The correct version has a lookup that decides a branch; this version has neither, so every call takes the same path and the second add cannot behave differently from the first. The empty stop is the finding, and it names the rule that lived there.

add Laptop on [ Laptop × 1 ], buggy version
  1. inputproductId = laptop, quantity = 1; cart = [ Laptop × 1 ]
  2. lookupMissing. Nothing looks for an existing entry with productId = laptop.
  3. branchMissing. With no lookup there is nothing to branch on; every call appends.
  4. mutationappend { laptop, 1 } to cart.items
  5. output[ Laptop × 1, Laptop × 1 ]. The rule "one entry per product" is not encoded; the fix is the lookup and the branch, not a filter somewhere downstream.

Why the fix must be the rule

A duplicate can be hidden in several places, and only one of the fixes is the rule. The table follows the same bug through the responses a reviewer might accept and shows what each leaves broken. Cart Lab ships this buggy variant; its invariant check catches the duplicate on the second add, and the exercise is to say which line is missing before the check says so.

One bug, four responses
TriggerSymptomCauseResponse
De-duplicate in the viewThe cart page looks right; changeQuantity still edits one of two entriesThe symptom was fixed where it was seen, not where the rule livesReject: the state is still wrong, and every other reader needs the same filter.
Sum quantities in totalThe total is right; the entries are still duplicatedSame — a reader compensating for a writerReject: total was already right by accident; nothing about the state improved.
Reject a second add as "already in cart"No duplicates; a shopper who clicks twice gets an errorA rule was encoded — the wrong one; the example says incrementReject: the add-again example fails; the rule chosen contradicts the decided after.
Find, then branch: increase or appendadd-again passes; changeQuantity has one entry to changeThe rule "one entry per product" encoded where the write happensAccept, with the add-again test as the definition of done.

The implementation ladder

Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.

concept Shopping Cart beginner
Build it step by step →

Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.

Identity, ownership, lifetime
  • Does a cart have identity? Yes, weakly. Two carts with the same items are still two carts, because each belongs to someone and will become a different order. It needs an id once it leaves memory; in memory the variable is the identity.
  • Who owns it? A shopper — a logged-in user or an anonymous session. The owner is part of the state because "my cart" has to be findable again.
  • How long does it exist? From the first add until checkout or abandonment. Whether it survives a reload, a closed browser or a login is not a property of the concept; it is a persistence decision made later, and each answer changes where the cart lives.
  • Should it survive reload? Usually yes for a store, usually no for a demo. V1 in memory says no; V2 browser storage says yes on one device; V3 server storage says yes everywhere the user is logged in.
  • Should it survive login? Only if an anonymous cart and a logged-in cart are merged — a rule that does not exist in V1 and appears as a modification later.
State it must remember
  • itemscollection of CartItemkeepThe cart is its items; without them nothing else means anything.
  • items[].productIdidkeepThe reference to what is being bought. The catalog owns the product; the cart only points at it.
  • items[].quantityinteger > 0keepTwo laptops is one entry with quantity 2, not two entries — the rule "one entry per product" needs a quantity to hold.
  • owneruser id or session iddependsSo the cart can be found again by the person it belongs to.
  • items[].productNamestringderiveIt would be convenient to render the cart without a catalog lookup.
  • items[].pricemoneydependsThe total needs a price per item.
  • totalmoneydropEvery screen shows the total.
  • currencycodedependsPrices need a currency to be added.
  • createdAttimestampdropAbandoned carts might be expired or emailed about.
Operations
  • update Add item the updated cart
  • delete Remove item the updated cart
  • update Change quantity the updated cart
  • read View items the list of entries — product id and quantity — for rendering
  • domain Calculate total the sum of price × quantity over the entries
  • delete Clear cart the empty cart
Rules that must always hold
  • Every quantity is greater than zero.
  • One logical entry per product.
  • The total is never negative.
  • An unknown product cannot be added.
  • Quantity cannot exceed available stock — if inventory is enforced here.

How to do it

Most important first.

  • For any operation you review, predict the edge example first; the normal case teaches nothing about a bug of omission (Normal, Edge, Invalid).
  • Trace the edge through input, lookup, branch, mutation, output; an empty lookup or branch stop on an operation that needs one is the finding (Input → Lookup → Branch → Mutation → Output).
  • State the bug as the rule that is not encoded, and check the other operations that rule touches — changeQuantity on a duplicated cart has no right answer.
  • Deliver the review as a failing test from the example, named for the behaviour, so the fix has a definition of done (Examples Become Tests).
  • Ask the author which example they wrote; if the answer is "the normal one", the missing rules are wherever the edges were not written (Why Is This a Map? — Detecting Cargo Cult and Routing Back).

Worked on a concrete problem

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

  • The buggy version from the record: cart.items.push({ productId, quantity }); return cart. Edge example: [Laptop × 1] + add Laptop. Prediction from the code: no lookup, so an append; the cart becomes [Laptop × 1, Laptop × 1]. Confirmed by running. Diagnosis: "one entry per product" is not encoded.
  • Consequences, predicted before running: the view shows Laptop twice; the total is right by accident (1 + 1 at the same price equals 2 at that price); changeQuantity(laptop, 3) finds the first entry and sets it to 3 while the second stays at 1 — a cart of Laptop × 3 and Laptop × 1 showing a total nobody chose. The bug is small; its downstream is not.
  • The review: the add-again test, failing, with the note "fromExample: add-again; rule: one entry per product". The fix is the find and the branch. A de-duplication in the view would have made the picture right and left changeQuantity broken.

How you know it worked

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

  • You can name the example a piece of code gets wrong before running it, and the trace stop that is missing.
  • The bug is stated as a rule and the fix is stated as a failing test.
  • You can predict the downstream damage — the operation that has no right answer once the rule is broken — not just the immediate wrong state.

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
  • ?Which example would this code get wrong — and can I say so before running it?
  • ?Which of the five trace stops is missing from this operation, and which rule lived there?
  • ?Once this rule is broken, which other operation has no right answer?
  • ?What failing test is this review, and what is its fromExample?

What can go wrong

How the move itself fails
  • Every review becomes a hunt for missing rules and the actual slip — < for <= — gets missed because it was present, not absent. Both readings are needed; this lesson is the one the reflex lacks.
  • The bug is predicted, confirmed, and fixed at the symptom — a filter in the view — because the diagnosis stopped at "duplicate" instead of reaching "rule not encoded".
  • The prediction is made after running, and the review says "I knew it" about a bug the run found. The prediction has to precede the run to be a review skill rather than hindsight.
What the move costs
  • Predicting the edge for every operation under review is slower than reading for slips, and most reviewed code has the find.
  • Stating bugs as rules can sound pedantic to an author who wanted "add a find" — and the pedantry is where the changeQuantity consequence gets noticed.
  • A review delivered as a failing test asks the reviewer to write code, and some teams keep review and implementation apart on purpose.
Misreads
  • "So the bug was a missing find." The bug was a missing rule; the find is one encoding of it, and the map representation encodes the same rule with no find at all. Naming the line instead of the rule is what makes the view-side filter look like a fix.
  • "Tests would have caught it." Only the add-again test would have, and it was not written because the example was not — tests catch what examples imagined. The lesson is upstream of the suite.
  • "Clean code has no bugs." Falsifiable: the three-line version is the cleanest addItem in the record and the only wrong one. Cleanliness is a property of what is present; correctness includes what is absent.

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.

  • GENERALPredicting the edge from the code is how to review any operation for a missing rule — a rate limiter without a window reset, a login without a lockout — and the trace with an empty stop is the same finding in each.
  • TEAM-SPECIFICA solo learner uses this on their own past code and on received code; a reviewer on a team uses it as the first question of a review, and a team under deadline uses it only on the operations whose rules they know to be subtle — the second add, the last unit.
  • ILLUSTRATIVEThe three-line buggy addItem, the teammate and the quantities are the concept record's invented predict-the-bug case; no real review is described.

Where the depth lives

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