State Shape From Examples
You cannot tell whether an item needs a quantity by staring at the class. Write the examples: Cart = [] → add Laptop → [Laptop × 1] → add Laptop → [Laptop × 2]. The second example is where quantity becomes a field and "two rows for the same product" becomes 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 state list has a shape in it — items, entries, fields — that you are guessing at. How do concrete before/after examples reveal the shape the state must have?
You wrote items: Product[] because a cart is a list of products. Adding works. Then you add the same laptop twice and the cart shows Laptop, Laptop. You are not sure whether that is a bug or just how carts work, and the class gives you no way to decide.
Decide by looking at other carts. Amazon shows a quantity; so the fix is to dedupe the list in the view, or to add a quantity field and hope nothing else breaks. Either one makes the screenshot look right.
Deduping in the view hides the shape problem: the state still holds two rows, changeQuantity has no entry to change, and the total is right only by accident. The screenshot is fixed and the state is wrong.
- Deduping in the view hides the shape problem: the state still holds two rows, changeQuantity has no entry to change, and the total is right only by accident. The screenshot is fixed and the state is wrong.
- Adding
quantitywithout an example to justify it leaves the rule unspoken. Nothing says whether adding Laptop again should increment the existing entry or append another with quantity 1 — and the code will do whichever the author happened to write. - The decision was made by imitation. Amazon has a quantity because Amazon's examples demanded it; you have not written yours, so you cannot say whether a wishlist (no quantity) or a cart (quantity) is what you are building.
- The cart looks done on screen and the shape of its state was never chosen — which is what the next bug will reveal.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Before deciding a field, write three or four before / operation / after examples in plain notation —
[],[Laptop × 1]— and let the examples say what the state has to be able to show. This is Examples Before Algorithms applied one step earlier, to state rather than to algorithms. - Look for the example where two candidate shapes disagree.
[]→ add Laptop →[Laptop × 1]is consistent with a plain list and with a list of entries;[Laptop × 1]→ add Laptop →[Laptop × 2]is not. That example decides the shape, and it is where the record says the rule "one entry per product" was discovered. - Read the shape off the examples: the "× 2" is a number attached to a product inside the cart, so an entry is a product reference plus a count. The examples also rule shapes out: a shape that can represent
[Laptop × 1, Laptop × 1]can represent a state the examples say must not exist. - Turn what the examples ruled out into a rule, and what they required into a field. Quantity is a field because an example needed it; "one entry per product" is a rule because an example forbade the alternative. The field and the rule arrive together, and the rule is what addItem will encode (Rules Come From Examples).
The example that decides the shape
Two of the record's examples, as a state change. The first is consistent with any shape. The second is the one that matters: the after-state has a count in it, so an entry must be able to hold a count, and it has one row, so the shape must not produce two.
[ Laptop × 1 ]
[ Laptop × 2 ]
Two shapes against the same example
The comparison is between the shape that a plain "list of products" gives and the shape the example demands. Both render fine after one add; only one is right after two.
items: Product[]. After add Laptop, add Laptop: [Laptop, Laptop]. Two rows; changeQuantity has no count to change; the view has to dedupe; the total is right by accident.
items: CartItem[] where CartItem = { productId, quantity }. After add Laptop, add Laptop: [{ laptop, 2 }]. One row; the count is where the shopper expects it; "one entry per product" is a rule the add can enforce.The example [Laptop × 1] → add Laptop → [Laptop × 2] has a state the first shape cannot represent without a convention layered on top; the second shape represents it directly, and the rule it needs — no duplicate product ids — is a single find before the append.
Asking the shape question well
The vague form of the question invites imitation; the best form names an example and asks what state must exist to show it. The same sharpening works for any field you are unsure about.
why The best form names an example with an after-state, which makes both the field (quantity) and the rule (one entry per product) answerable at once; the vague form can only be answered by looking at someone else's cart.
The implementation ladder
Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.
Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.
- 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.
- 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.
- 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
- • 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.
- Write the examples in the record's notation — before, operation, after — with no code.
[ Laptop × 2, Mouse × 1 ]is readable by the person who asked for the cart, which is the point. - Include the example you are unsure about. "Add the same product twice" is the one that decides the shape; leaving it out because it is awkward is leaving the shape undecided.
- For each candidate shape, check every example: can this shape represent the after state, and can it also represent states the examples forbid? A shape that permits forbidden states will need a rule in code to prevent them (Array Cart vs Map Cart).
- Add the zero case:
[Laptop × 2]→ change Laptop to 0 →[]. It reveals that quantity 0 is not a state the cart holds, which is a rule about the entry's type: integer > 0. - Keep the examples; they become tests in Examples Become Tests without translation.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The record's examples in order:
Cart = []→ add Laptop →[ Laptop × 1 ];[ Laptop × 1 ]→ add Laptop →[ Laptop × 2 ]with the note "Not two entries. This example is where the rule 'one entry per product' was discovered.";[ Laptop × 2 ]→ add Mouse →[ Laptop × 2, Mouse × 1 ]. - What the second example decided: the entry is
{ productId, quantity }, not a product. The fielditems[].quantitygets the verdict keep with the reason "two laptops is one entry with quantity 2, not two entries — the rule 'one entry per product' needs a quantity to hold." - The example
[ Laptop × 2 ]→ change Laptop to 0 →[]with the note "a cart never holds quantity 0, so the change becomes a removal" fixes the type of quantity as integer > 0 and turns changeQuantity(…, 0) into a call to removeItem — a decision the class alone would never have forced.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The state list has an example next to every field that is not obvious, and the example is what someone would point to when asked "why is quantity there?"
- You can name the example that would break if the shape were changed — and the shape a wishlist would need instead.
- The rule "one entry per product" exists in words before it exists in code, with the example that produced it.
- The examples are already the first three tests.
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.
- ?Which example would two candidate shapes represent differently?
- ?Which after-states can my shape represent that the examples say must never exist?
- ?Which field exists only because an example needed it — and which example?
- ?What does the zero case, the empty case and the repeat case each reveal about the entry's type?
What can go wrong
- The examples are all happy-path and all consistent with every shape, so they decide nothing. The useful example is the one where shapes disagree; write the awkward case.
- The shape is read off the examples and the rule is not: quantity is added, but nothing prevents two rows for the same product, and the Predict the Bug case — addItem that always appends — ships.
- Examples are written in code instead of notation, and the notation's chief virtue — that a non-programmer can confirm "yes, that is what a cart does" — is lost.
- The examples are written after the class, to match it, so they confirm the guessed shape instead of testing it.
- Writing examples before the state delays the class by a few minutes and a few lines of notation; on a concept whose shape is genuinely obvious, the minutes were spent confirming the obvious.
- Examples pin behaviour early. A cart whose examples say "one entry per product" cannot later quietly become a list that allows duplicates — which is the point, and also a cost if the requirement was wrong.
- A shape chosen from examples may permit forbidden states (an array can hold two Laptop rows) and needs a rule in code; the alternative shape (a map) makes the rule structural at the price of ordering and serialisation. Examples reveal the choice; they do not make it.
- "Examples are for testing, later." Examples are how the shape is discovered; testing is what they do afterwards. Writing them last means the shape was guessed.
- "The examples say the state is a list, so it is an array." The examples say the state is an ordered collection of entries with unique product ids; array and map both satisfy that, and the choice is Choosing a Representation.
- "Quantity is obvious; nobody needs an example for it." Then a wishlist, a set of favourites and a list of recently-viewed products all get a quantity too, because "obvious" was never checked against an example that lacked one.
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.
- GENERALReading state shape off before/after examples works for any concept whose state changes — messages, bookings, job queues — because the decisive example is always the one where two candidate shapes would show different after-states.
- ILLUSTRATIVEThe laptop, the mouse and the six examples are the shopping-cart record's; a real cart would write its own examples, and the decisive one is still "add the same product twice".
Where the depth lives
This domain asks the question and hands the answer off by name.