To CodeGENERALTEAM-SPECIFICILLUSTRATIVE

Implement One Operation

Only now, real code — and only addItem. Two types, one function, in TypeScript, transcribed from the pseudocode line for line; the other five operations wait. One operation is enough to run the first three examples, find the first mistake in the transcription, and learn what the language did to the pseudocode — all before there is much code to be wrong.

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

The pseudocode is done. Which operation do you implement first, why only one, and what should you learn from it before writing the second?

The situation

You have six pseudocode functions and a language. Writing all six feels like the obvious next step — they are right there — and stopping after one feels like leaving the job half done.

The reflex

Implement everything at once, top to bottom, then run the examples at the end. If the pseudocode is right the code will be right, and running early is just running more often.

Why it stalls

Six functions arrive together and the first test fails. Is it the transcription of addItem, the shared find idiom, the Cart type, or changeQuantity calling removeItem? Six suspects for one symptom, and the examples cannot tell them apart because they were all run for the first time at once.

What the reflex produces — and fails to produce
  • Six functions arrive together and the first test fails. Is it the transcription of addItem, the shared find idiom, the Cart type, or changeQuantity calling removeItem? Six suspects for one symptom, and the examples cannot tell them apart because they were all run for the first time at once.
  • The language's decisions — find returning undefined, push mutating, === — were made six times without being noticed once. The second function copies the first's idiom before anyone has checked that the idiom is the pseudocode's find … with ….
  • There is no working thing until the end. For the whole afternoon nothing can be run, nothing can be shown, and the pseudocode cannot be trusted or distrusted because no line of it has met an example in code.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Pick the operation with the most decisions in it and implement only that one. For the cart it is addItem: it has both checks, the find, the branch, and the append — every shape the other five use, in one function. clear teaches nothing; addItem teaches the language's find, the language's mutation, and the language's way of rejecting (Implementation Order).
  • Transcribe the pseudocode line for line into the language, keeping the comments that name the rules. The record's TypeScript is the eight pseudocode lines with the language's syntax for each: catalog.has, throw new Error, cart.items.find((i) => …), existing.quantity += quantity, cart.items.push. Add the two types the function needs and nothing else (Meaning Before Representation).
  • Run the first three examples against it — add-first, add-again, invalid-zero — before writing anything else. Three examples, one function, and any failure has exactly one suspect. The test for add-again is two lines and it is the rule "one entry per product" meeting the language for the first time (Examples Become Tests).
  • Write down what the language did to the pseudocode. find returns undefined, so if item exists became if (existing). push mutates, so the cart is changed in place and returned. throw replaced reject. Those three notes are what the next five functions inherit — and they are notes about the language, not about the cart.

Which operation, and why only one

The order is a choice with a reason at each step, and the alternative — breadth first — is what most people do and is right in one specific circumstance. The reason for addItem first is that it contains every shape the other five use; the reason for stopping is that a failing example then has one suspect.

The first hour of code
  1. 1
    addItem only, with the two types it needs

    because It has both checks, the find, the branch and the append — every shape the other five operations use, in one screen.

  2. 2
    Transcribe line for line, keeping the rule comments

    because A line with no pseudocode behind it is a decision made in the language's vocabulary, and the comment on the find is the rule's name kept in the code.

  3. 3
    Run add-first, add-again, invalid-zero

    because Three examples against one function: any failure has one suspect, and add-again is the rule "one entry per product" meeting the language.

  4. 4
    Write down what the language did

    because find → undefined, in-place mutation, throw for reject — three facts the next five functions inherit without re-deciding.

  5. 5
    Then removeItem, changeQuantity, total, getItems, clear

    because Each is written against the notes and its own three examples, and each takes minutes because the shapes are already known.

a different valid order Breadth first — all six functions as stubs that throw "not implemented", then fill them in one at a time. Choose this when the operations call one another (changeQuantity calls removeItem) and you want the call graph to compile from the start; it costs the clean single-suspect property, because the stubs are also code, and it is what a team does when several people take one function each.

The one operation, in the language

The record's TypeScript for addItem, with the types it needs and nothing else. Every line has a pseudocode line behind it; the comment on the find is the rule. Notice what is absent: no other operation, no owner, no persistence, no framework.

addItem, transcribed
1type ProductId = string
2interface CartItem { productId: ProductId; quantity: number }
3interface Cart { items: CartItem[] }
4
5const catalog = new Set<ProductId>(['laptop', 'mouse', 'keyboard'])
6
7export function createCart(): Cart {
8 return { items: [] }
9}
10
11export function addItem(cart: Cart, productId: ProductId, quantity = 1): Cart {
12 if (!catalog.has(productId)) throw new Error('unknown product') // 1
13 if (quantity <= 0) throw new Error('quantity must be positive') // 2
14 const existing = cart.items.find((i) => i.productId === productId) // 3: one entry per product
15 if (existing) existing.quantity += quantity // 4
16 else cart.items.push({ productId, quantity }) // 5
17 return cart // 6
18}

Three things the language decided that the pseudocode did not: find returns undefined when nothing matches, so "exists" is truthiness; += and push mutate the cart in place; throw is how this language rejects. The find is an O(n) scan over the items — invisible for a cart, and the line a map would replace.

The first examples against the first function

The tests the record derives from its examples, for this operation only. The trace beneath is the second test run by hand through the *code* rather than the sentences — the same five stops, now with the language's names on them, which is how you check the transcription rather than the algorithm.

add-again, through the code
  1. inputThe inner call returned { items: [{ productId: 'laptop', quantity: 1 }] }; the outer call receives it with productId 'laptop' and quantity 1.
  2. lookupcart.items.find((i) => i.productId === productId) scans one entry and returns it — existing is the object, not undefined.
  3. branchif (existing) is truthy; the else with the push does not run.
  4. mutationexisting.quantity += quantity changes 1 to 2 on the object inside the array — in place, which is why cart.items shows it.
  5. outputThe same cart object, now [{ laptop, 2 }]; deepEqual passes.
add-again and invalid-zero, as tests
1// from example add-again
2const cart = addItem(addItem(createCart(), 'laptop'), 'laptop')
3assert.deepEqual(cart.items, [{ productId: 'laptop', quantity: 2 }])
4
5// from example invalid-zero
6const empty = createCart()
7assert.throws(() => addItem(empty, 'laptop', 0), /positive/)
8assert.deepEqual(empty.items, [])

Two examples, four assertions, one function. If the first fails there is one place to look; if the second fails, the state check is the one that matters — a rejection that left a partial entry behind is a worse bug than a wrong message.

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.

  • Choose the operation that exercises the most shapes; it is usually the create-or-update, not the read (CRUD and Domain Actions).
  • Transcribe, do not compose: one pseudocode line, one or two code lines, with the rule comments kept. The find gets the comment "one entry per product".
  • Declare only the types this operation needs — here ProductId, CartItem, Cart — and nothing for the operations that wait.
  • Run the normal, edge and invalid examples as tests against this one function before starting the second (Normal, Edge, Invalid).
  • Record the language's three or four decisions as a short list; the second operation is written against that list, not against a fresh guess.

Worked on a concrete problem

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

  • addItem in TypeScript, from the record: if (!catalog.has(productId)) throw …, if (quantity <= 0) throw …, const existing = cart.items.find((i) => i.productId === productId) // one entry per product, if (existing) existing.quantity += quantity, else cart.items.push({ productId, quantity }), return cart. Six lines for eight pseudocode lines; the types ProductId, CartItem, Cart above it; a catalog set with three ids for the check. Nothing else in the file.
  • The tests: add-again — addItem(addItem(createCart(), 'laptop'), 'laptop') and items equals [{ productId: 'laptop', quantity: 2 }]; invalid-zero — addItem(cart, 'laptop', 0) throws with "positive" and items is still []. Both pass on the first run, or one fails and there is exactly one function to look at.
  • What the language did: findundefined when absent, so "exists" is truthiness; push and += mutate in place, so the function changes its argument and returns it; throw new Error(message) is reject. Three notes. removeItem is then written in a few minutes as filter, against the same notes, and changeQuantity reuses find with the same truthiness check.

How you know it worked

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

  • One function exists, with the types it needs and nothing more, and the three examples for it pass.
  • There is a short list of what the language did to the pseudocode, and it is about the language, not the cart.
  • The second operation is written faster than the first and against the list, and its examples pass on the first run.

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 operation has the most decisions in it, and is that the one I am implementing first?
  • ?Does each line of code have a pseudocode line it transcribes — and do the rule comments survive?
  • ?Do the normal, edge and invalid examples pass against this one function?
  • ?What did the language do to the pseudocode, and have I written it down for the next function?

What can go wrong

How the move itself fails
  • The one operation chosen is the easiest — clear — and teaches nothing; the hard decisions arrive with the second function anyway, now without a working baseline.
  • "One operation" becomes "one operation plus the types for all six plus the persistence interface", and the function cannot be run until the scaffolding exists.
  • The operation is implemented and the examples are not run, so the second function inherits a find idiom that nobody checked; the point of stopping was to run.
What the move costs
  • One function at a time is slower in wall-clock on a concept whose six functions are all trivial; the discipline pays on the first unfamiliar one and costs on the familiar five.
  • Choosing the most complex operation first means the first hour is the hardest hour, with no easy win to build confidence on.
  • The language notes are a small artefact that a solo learner keeps and a team forgets; their value is in the second function, and after the sixth they are folklore.
Misreads
  • "Implement one operation" means build a vertical slice. A slice cuts through layers — UI to database — for one feature; this cuts through nothing. It is one function in memory with its examples, and the layers come in the engineering half (Implementation Is Not Engineering).
  • "If the pseudocode is right the code will be right." The pseudocode being right is what the first three examples in code are for; until they pass, "right" is a belief.
  • "Start small" means start with the simplest function. The falsifiable form is "start with the function whose failure would teach the most and whose size is still one screen" — addItem, not clear.

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.

  • GENERALOne operation first — the one with the most shapes in it — then its examples, then notes on what the language did, is the same move for any concept in any language; only which operation is "richest" changes.
  • TEAM-SPECIFICA learner stops after one function and runs the examples; an engineer fluent in the language writes several in one sitting because the language notes are already in their head — and still runs the examples after the first, which is the part that does not go away with experience.
  • ILLUSTRATIVEThe six lines, three tests, three ids in the catalog and the few minutes for removeItem are the concept record's invented example, given for the shape of the argument; no real codebase is described.

Where the depth lives

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