Rules Come From Examples
The example "[Laptop × 1], add Laptop → [Laptop × 2]" is where the rule "one entry per product" was discovered. Rules you write down in the abstract are the obvious ones; the ones that shape the code arrive when you write what should happen and notice you had to decide.
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.
You listed the rules you could think of and the list feels thin. Where do the rules you did not think of come from?
You wrote "quantity > 0" and "no unknown product" and stopped, sure there were more. You stare at the cart and nothing else looks like a rule. Then someone asks what happens when the shopper clicks Add to Cart on the same laptop twice, and you realise you have no answer — and that whichever answer you give is a rule.
Search for "shopping cart business rules" and collect a list. It is long, it mentions coupons and tax, and it looks like the work is done.
The list is someone else's cart. It has rules for coupons this store does not have and none for the question that was actually asked, because "same product twice" is too ordinary to appear on a list — it only appears when you try to write down what happens.
- The list is someone else's cart. It has rules for coupons this store does not have and none for the question that was actually asked, because "same product twice" is too ordinary to appear on a list — it only appears when you try to write down what happens.
- The abstract rules were the obvious ones. Quantity > 0 and "product exists" are on every list; "one entry per product" is on none, and it is the rule that decides whether
addItemhas a find in it. - With the list in hand, the examples never get written, so the decisions the examples force — is the second add an error, a duplicate, or an increment? — are made by whichever line the code happens to have.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Write concrete before → operation → after examples for every operation, and pay attention at the moments you hesitate. Hesitation is the signal: it means the outcome is not determined by anything you have written yet, and the sentence that resolves it is a rule.
- At each hesitation, say the alternatives out loud. Same product added twice: two entries, one entry with quantity 2, or an error? Each is a possible rule; choose one and write it in the state-sentence form — "one logical entry per product". Then write the example's
afteraccording to it. - Go back to the abstract rules and check each against the examples. A rule no example touches is either derived — the total is never negative — or a rule for a version you are not building. A rule several examples touch is central, and the examples become its tests.
- Keep the example next to the rule. The record does this in its notes: "this example is where the rule was discovered". When someone later asks why
addItemhas a find in it, the example is the answer, not a paragraph.
The example where the rule was found
The before and after below are from the concept record, and the record's own note on this example reads: "Not two entries. This example is where the rule 'one entry per product' was discovered." Nothing about the abstract cart demanded a find-before-append; writing the after did, because the author had to choose between [Laptop × 1, Laptop × 1] and [Laptop × 2] and could not leave it blank.
Cart = [ Laptop × 1 ]
Cart = [ Laptop × 2 ]
Hesitations as unknowns
Each hesitation is an unknown of the ordinary kind: a vague feeling that becomes a specific question with a tiny experiment — here, an example whose after you have to write. The board lists what the abstract rules already settled, and the three hesitations the examples produced.
- ✓Every quantity is greater than zero — settled abstractly, confirmed by the invalid example.
- ✓An unknown product cannot be added — settled abstractly; the check goes before the find.
- ✓The total is never negative — derived; no example touches it directly.
- ~One shopper, one process — the two-tabs case is a V3 rule, not a V0 one.
? What if they add the same thing twice?
becomes When a product already in the cart is added again, is the result two entries, one entry with the summed quantity, or an error?
experiment Write the example [Laptop × 1] + add Laptop and fill in the after; then ask which after changeQuantity could operate on.
? Zero is weird.
becomes Is a change to quantity 0 a stored entry, a removal, or a rejection — and which readers would have to handle a zero if it were stored?
experiment Write [Laptop × 2] + change to 0 with all three afters, and count the functions that would need a filter under the first.
? Removing something that is not there.
becomes Is removing an absent product an error or a no-op, and does the answer change when the caller is an API instead of a function?
experiment Write the example with both afters; note that V1 chooses no-op and that an HTTP DELETE would still return success.
Asking the question that finds the rule
The generic list answers "what are a cart's rules?" and misses the second add. The question that finds it is narrower and concrete, and the failure table shows what each missing rule looks like once it reaches a running cart — the symptoms are how you recognise, in someone else's code, that an example was never written.
why The best form forces a decision on a concrete case and produces both the rule and its first test; the vague form produces a list that belongs to no particular cart.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Same product added twice | Laptop appears twice in the cart view | No example for add-again, so no find before the append | Write the example; encode "one logical entry per product"; test from the example. |
| Quantity changed to 0 | A line reading "Laptop × 0" in the cart and the total | No example for to-zero, so the change is stored | Write the example; convert a change to 0 into a removal. |
| Remove clicked on a stale cart | An error for removing something already gone | No decision on absent removal; the default was to throw | Decide no-op or error explicitly; write it down with the API's answer beside it. |
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 happy-path examples first, then the ones where you had to decide; the second kind is where the rules are (Finding Invariants From Examples).
- When you hesitate, write down every alternative outcome before choosing one. The one you did not choose is a rule too — the rule you rejected — and worth a note.
- Turn each example-born rule into the three lines rule → validation → code (From Invariant to Validation), then back into the example as a test (Examples Become Tests).
- Run each abstract rule through the examples: which example would break if it were dropped? None means derived, delegated, or not yet.
- Ask the same-twice question of every operation: remove twice, clear twice, change to the same quantity. Repeats are where rules hide.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Cart = [Laptop × 1], add Laptop. Hesitation. Alternatives: [Laptop × 1, Laptop × 1]; [Laptop × 2]; error "already in cart". A shopper who clicks twice means "two", so [Laptop × 2] — and the rule "one logical entry per product" is born, with its find-before-append.
- Cart = [Laptop × 2], change Laptop to 0. Hesitation. Alternatives: [Laptop × 0]; []; error. An entry with quantity 0 is a thing every reader would have to skip, so [] — and the rule "a cart never holds quantity 0" arrives, turning the change into a removal.
- Cart = [Laptop × 2, Mouse × 1], remove Keyboard. Hesitation. Alternatives: error; no-op. The caller wanted a cart without a keyboard and has one, so no-op — a rule about the response, written down as V1's choice with the note that an API might choose 404.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every rule on the page has an example beside it that would break if the rule were dropped, and a note saying which example produced it.
- The rules list has entries that were not on any generic list — the ones about repeats and zeros — and you can say what alternative each one rejected.
- You can answer "what happens if…?" about any operation without looking at the code, because the example already decided 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.
- ?Where, in writing the examples, did I hesitate — and what sentence resolves the hesitation?
- ?Which alternatives did I reject at that moment, and did I write them down?
- ?Which of my abstract rules does no example touch, and is it derived, delegated, or for a later version?
- ?What happens when this operation runs twice with the same input?
What can go wrong
- Examples are written to confirm the abstract rules rather than to find new ones: every example uses a fresh cart and a positive quantity, and no hesitation ever occurs. The move needs the awkward cases — repeats, zeros, absences.
- Every hesitation becomes a rule, including ones about presentation. "Items appear in the order added" is a property of the array and a UI preference, not a rule the cart must defend; writing it as one produces a check that protects nothing.
- The chosen alternative is written down and the rejected ones are forgotten, so the next engineer re-decides "same product twice" from scratch and picks the duplicate.
- Examples take longer to write than a list of rules, and on a well-worn concept most of them confirm what you already knew.
- Rules found from examples are only as complete as the examples; a case you never wrote — two tabs adding at once — is a rule you never found.
- Keeping the example beside the rule is a documentation habit that has to survive the move from a page to the code, and often does not.
- "So the rules are whatever the examples say." The example records a decision; the rule is the sentence that generalises it. [Laptop × 2] is one example; "one logical entry per product" covers the mouse, the keyboard and every product not yet in the catalog.
- "Generic rule lists are useless." They are a prompt, not an answer. Reading one is a fast way to notice a hesitation you had not reached yet — tax, coupons — as long as each entry is then tested against your examples rather than copied.
- "If I write enough examples I will find every rule." You will find every rule the examples exercise. Concurrency, persistence and scale add rules that single-process examples cannot reveal; they arrive with those versions.
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.
- GENERALHesitation while writing a concrete example is a rule-detector for any concept — a rate limiter's "what happens on the request that lands exactly at the window boundary?" is the same move as the cart's second add.
- SIMPLIFIEDSingle-process, single-shopper examples are used throughout; rules that only appear under concurrency or persistence — two tabs, a stale stored cart — are deliberately left to the versions that introduce them.
- ILLUSTRATIVELaptop, Mouse and Keyboard and their quantities are the concept record's invented example, chosen to make the hesitations visible.
Where the depth lives
This domain asks the question and hands the answer off by name.