Understanding Is Demonstrated by Modification
Five new requirements — a maximum of 5 per product, several carts per user, a cart that survives reload, an anonymous cart merged at login, inventory checked on add. Each breaks a named assumption and touches a named set of artefacts. If you can say which before touching the code, you understood the cart; if you can only find out by breaking it, you received it.
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.
A requirement lands on your working cart. Before editing anything, can you name the assumption it breaks and every artefact it touches — rules, examples, lines, tests, persistence, API — and what does it mean if the list is a guess?
The cart works. The product owner says "a shopper cannot add more than five of anything". You open addItem and start typing an if statement, and then realise you do not know whether changeQuantity needs the same check, whether there is an example for it, or where the number five should live.
Add the if. It is one line in addItem, the requirement said "add", and the tests still pass — which they would, since none of them adds six of anything. Ship it, and handle changeQuantity when someone notices.
The tests pass because the examples do not include the new edge. A modification whose tests still pass unchanged has not been tested; it has been left alone by a suite that predates it.
- The tests pass because the examples do not include the new edge. A modification whose tests still pass unchanged has not been tested; it has been left alone by a suite that predates it.
- The assumption that broke was "any positive quantity is valid", and it was encoded in two operations and one rule. The if fixed one encoding. Someone will set the quantity to 20 through changeQuantity, and the rule will be found to have two homes, one of them empty.
- The number five is in the code and nowhere else — not in a rule, not in an example, not in a test name. The next requirement ("five, except for bulk items") has nothing to attach to.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Before any edit, write the requirement as a change to the concept, not to the code: which rule is new or altered, which examples are new (an edge at the limit, an invalid case past it), which operations enforce it, which tests come from the new examples, and whether state, persistence or the API move. The concept's
modificationsrecord does exactly this for five requirements, and the list of what each one touches is the answer key. - Name the assumption the requirement breaks. Maximum quantity breaks "any positive quantity is valid". Several carts break "one cart per owner". Survive reload breaks "the cart only exists while the page does". Merge at login breaks "one owner for the cart's whole life". Inventory breaks "the cart is the only authority on what can be added". The assumption tells you how far the change reaches: a rule change reaches operations and tests; an owner change reaches state, every operation's signature, the API paths and persistence (When Assumptions Change).
- Then edit in the loop's order: rule, examples, flowchart placement, pseudocode, code, tests. The if is the fifth step, and by then it is obvious where it goes — after the find, because the check needs the existing quantity — and that changeQuantity needs it too.
- Judge the modification by whether the understanding check still passes afterwards. Question six of the check is this lesson; if the answers to questions two and five did not change when the cart moved to a database, the persistence modification was pasted (Six Questions the Working Cart Cannot Answer for You).
Rule → validation → code, for the new rule
The maximum-quantity requirement, run through the rule device before any code is touched. The validation line is where the placement decision lives — "after the find" — and the code is what that sentence becomes. Both operations that enforce it are in the pseudocode, because the rule has two homes and the device makes that visible.
rule No entry in the cart has a quantity greater than 5 — the assumption "any positive quantity is valid" is withdrawn.
↓ becomes validation In addItem, after finding the existing entry, reject if existing quantity plus the added quantity exceeds 5 (or the added quantity alone, when there is no entry). In changeQuantity, reject if the new quantity exceeds 5. The limit is named once and read by both.
MAX_PER_PRODUCT = 5 -- in addItem, after the find: newQuantity = (existing ? existing.quantity : 0) + quantity if newQuantity > MAX_PER_PRODUCT: reject "at most 5 per product" -- in changeQuantity, after the zero case: if quantity > MAX_PER_PRODUCT: reject "at most 5 per product"
The edge, seen as a state change
The two new examples, as before / operation / after. The edge case at the limit succeeds and the changed list has one line; the invalid case past it leaves the state untouched and the changed list says so. Both lists become assertions, and the second is the one an off-by-one would break.
[ { productId: "laptop", quantity: 5 } ][ { productId: "laptop", quantity: 5 } ]Five modifications, five reaches
The matrix quotes the concept's modifications — the assumption each one breaks and what it touches — and adds the column that predicts the size of the work: whether the change is a rule, an operation, or a signature. Reading down the last column is the lesson: the same word, "add", lands on five very different amounts of cart.
| Change (from the concept) | Breaks | Touches | Kind of reach |
|---|---|---|---|
| Maximum quantity per product is 5. | any positive quantity is valid | rules; examples (edge at 5, invalid at 6); addItem and changeQuantity; tests | a rule — two operations, four tests |
| A user can have several named carts. | one cart per owner | state (cart id and name); every operation takes a cart id; API paths; persistence | every signature — wide and shallow |
| The cart must survive a reload. | the cart only exists while the page does | persistence (browser storage); load on start; the stale-product failure mode | a wrapper — no operation changes, one new failure mode |
| An anonymous cart merges into the user's cart at login. | one owner for the cart's whole life | a merge operation with a rule for overlapping products; examples for the overlap; API (login hook); tests | a new operation with a decision the requirement did not make |
| Adding checks inventory. | the cart is the only authority on what can be added | rules (stock); addItem asks inventory; the race between two shoppers and the last unit; failure modes | a rule that lives elsewhere — and a race |
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.
- Take one modification from the concept's five and, with the code closed, write: assumption broken, rules touched, examples added, operations touched, tests added, anything in state / persistence / API. Then open the record and compare.
- Write the new examples first, always. Maximum 5: [ Laptop × 4 ] + add Laptop → [ Laptop × 5 ] (edge); [ Laptop × 5 ] + add Laptop → rejected, cart unchanged (invalid). The tests come from these (Normal, Edge, Invalid).
- Place the new diamond on the flowchart before the new line in the code; the placement decides which operations get it and what must be looked up first (Draw the Branches Before You Trust Them).
- Put the number in one place with a name — a rule, a constant — and reference it from both operations, so the next requirement changes one thing.
- Do the reload modification in the lab and watch which answers in the understanding check move: what breaks if persistence appears is now a real question with a stale-product answer.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Maximum 5 per product. Assumption broken: any positive quantity is valid. Rule: quantity per product ≤ 5. Examples: edge at 5, invalid at 6 — for add on an existing entry (4 + 1, 5 + 1) and for change (set to 5, set to 6). Operations: addItem, after the find, on existing + quantity; changeQuantity, on the new quantity. Tests: four, from the four examples. State, persistence, API: untouched, except that the API's 400 now has a new wording. The concept's record lists rules, examples, the two operations and tests — the same four.
- Several named carts per user. Assumption broken: one cart per owner. State: a cart id and a name; the owner now has many. Operations: every one takes a cart id — the signature of all five changes. API: /cart becomes /carts/:id. Persistence: the cart table needs the name and the owner index. Rules: none new — the per-cart rules are unchanged. The reach is wide and shallow: many files, no new logic. A learner who "adds a carts array" has done the state and missed the signatures.
- Anonymous cart merges at login. Assumption broken: one owner for the cart's whole life. A new operation, merge(anonymous, owned), with a rule for overlapping products — sum the quantities, or keep the owned one, a decision the requirement did not make. Examples for the overlap: [ Laptop × 1 ] merged into [ Laptop × 2 ] → [ Laptop × 3 ] under the sum rule. API: a hook at login. Tests from the examples. This one adds an operation; the maximum-quantity one added a rule; the difference in reach is the difference in the assumption.
- Inventory checked on add. Assumption broken: the cart is the only authority on what can be added. Rule: quantity ≤ available stock — enforced by asking the inventory service, at add and again at checkout. The new failure mode: two shoppers and the last unit, a race the cart cannot resolve alone; the concept's rule says so, and the concurrency and database links are where the resolution lives.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The list of touched artefacts was written before the code was opened and matched the concept's record, or differed for a reason you can state.
- New examples exist at the edge and past it, and the tests were derived from them, not from the code.
- The number lives in one named place, and both operations that enforce it reference it.
- The reach of each modification — a line, an operation, every signature — was predicted from the assumption it broke.
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 assumption does this requirement break, and how far does that assumption reach — a rule, an operation, every signature?
- ?What are the new examples at the edge and past it, before any line is written?
- ?Which operations enforce the new rule, and where in each one does the check go — what must be looked up first?
- ?After the change, which answers in the understanding check moved, and should they have?
What can go wrong
- Editing the code first and discovering the reach by breakage. The if goes in, the tests stay green, and the second operation is found by a bug report.
- Adding a rule without an example. A rule with no edge example has no test at the boundary, and the boundary — 5 allowed, 6 rejected — is exactly where an off-by-one lives.
- Treating every modification as the same size. Maximum quantity is one rule; several carts is every signature. Predicting the reach from the assumption is what stops the second being estimated like the first.
- Over-generalising early. "Five, except for bulk items" has not been asked for; a rules engine for per-product limits is a forecast, and the concept's versions say what arrives when.
- Writing the touch list before the edit is slower for a one-line rule; it is the same minute that finds the second operation the one line missed.
- A named constant for a number used twice is a small indirection for a small gain; it becomes worth it at the third use or the first exception.
- Predicting reach from assumptions is a judgement and it will be wrong sometimes — the record is a guide for five modifications, and the sixth is yours to reason about.
- "So a modification is a checklist: rule, example, code, test." It is the loop's order applied to a change; the checklist form is the shape, and the content — which rule, which example, which operation — is the understanding. Two people can follow the checklist and only one of them finds changeQuantity.
- "If the tests pass, the modification is done." The tests that exist predate the requirement; they cannot fail on it. The tests that would fail are the ones derived from the new examples, and until they exist the modification is unverified.
- "Understanding means I can make any change quickly." It means the reach of a change can be predicted before it is made. A wide change is still slow; it is no longer a surprise.
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.
- GENERALNaming the broken assumption and listing the touched artefacts before editing applies to any change to any concept; the five modifications here are the cart's, and every concept in the catalog carries its own set.
- SCALE-SPECIFICThe inventory modification is the one whose reach depends on scale: one server and a small store enforce it at checkout and accept the race; a store where the last unit matters resolves it with the database and the concurrency lessons, and the cart's rule says where.
- ILLUSTRATIVEThe maximum of 5, Laptop × 4 → Laptop × 5, and the merge sum are for the shape of the argument; the five modifications and what each touches are quoted from the concept record.
Where the depth lives
This domain asks the question and hands the answer off by name.