Before, Operation, After — and Exactly What Changed
A state change you cannot describe as "this field went from this to that" is one you have not seen. Put the state before, the operation, and the state after side by side, and name every difference — there are usually fewer than you think, and occasionally one more.
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 know the cart went from one Laptop to two. Can you list every field that changed, every one that did not, and say which line of the operation is responsible for each — and what would you miss if you could not?
The cart lab shows two JSON blobs, before and after, and they look almost the same. You can see the 1 became a 2. You are fairly sure that is all, but the blob has an items array and objects inside it, and "fairly sure" is not the same as having checked.
Eyeball the diff. Two screenshots, one glance, spot the number that moved, done. If a tool highlights it in green, so much the better — the highlighted cell is the change and everything else is noise.
The eyeball finds the change it expected and stops. It does not find the change it did not expect — an entry appended in addition to the increase, an items array replaced by a new array with equal contents, an owner field that was silently dropped by a serialise-and-load.
- The eyeball finds the change it expected and stops. It does not find the change it did not expect — an entry appended in addition to the increase, an items array replaced by a new array with equal contents, an owner field that was silently dropped by a serialise-and-load.
- Two screenshots say what differs, not why. "Quantity is 2" is a fact about the after; "quantity 1 → 2 because the increase branch ran" is a fact about the operation, and only the second helps when the after is wrong.
- The unchanged fields are never listed, so nobody notices when one of them should have changed. A change-quantity that sets the quantity and forgets to remove the entry at zero looks fine in the after — until you write down "items: unchanged" and realise it should not be.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Lay the three columns out: the state before, the operation with its exact arguments, the state after. Then produce the fourth thing, which is the point: a list of what changed, written as field: old → new. The list is a claim about the operation; check it against the after column field by field, including the fields that should not have moved.
- Name changes at the level the rule cares about. "Laptop.quantity 1 → 2" and "items.length unchanged" are the two lines that together say "one entry per product held". A diff tool shows the first; the second is the invariant, and you write it yourself.
- Attribute each change to a line. The 1 → 2 is
existing.quantity += quantity; if you cannot point at the line, either the change is coming from somewhere you did not write, or the line does more than you thought. Both are worth knowing before the operation is trusted. - Do it for the edge and invalid examples too. The invalid one has an empty changed list, and writing "changed: nothing" is a stronger statement than not writing anything — it is a claim the test can assert (Examples Become Tests).
The change the rule protects
The normal case, with the changed list written out. One field moved. The second line under it — the number of entries did not — is not a change; it is the rule "one logical entry per product" being observed to hold, and it is the line that distinguishes this after from the buggy variant's.
[ { productId: "laptop", quantity: 1 } ][ { productId: "laptop", quantity: 2 } ]existing.quantity += quantity on the increase branch. · items.length: unchanged at 1 — the rule "one entry per product", holding. The always-append variant would show 1 → 2 here. · items[0].productId: unchanged — the entry was found, not replaced.The change that is a removal in disguise
The edge case. Read naively, the operation is "set the quantity to 0" and the expected changed line is quantity 2 → 0. The concept's rule forbids that state, so the operation delegates to removeItem, and the real changed line is about the number of entries. Writing the list is how the disguise comes off.
[ { productId: "laptop", quantity: 2 } ][ ]
if quantity == 0: return removeItem(cart, productId), not by any assignment to quantity. · items.length: 1 → 0. · no entry has quantity 0 — the rule "every quantity is greater than zero", holding by construction rather than by a check.The change that must be nothing
The invalid case, and the reason "changed: nothing" is written rather than left blank. The test that came from this example asserts two things: the error, and that items still deep-equals the empty list. The second assertion is this changed list. An operation that validated after a partial write would pass the first assertion and fail the second — which is why the check order in the trace lesson matters (Input → Lookup → Branch → Mutation → Output).
[ ]
[ ]
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.
- For every example in the concept — there are six — write the changed list under it. Most have one line; to-zero has two (the entry is gone, the length dropped); invalid-zero has none.
- Add an "unchanged" line for anything the rules protect: the number of entries after an add of an existing product, the other entries after a remove, the whole state after a reject.
- Predict the changed list before running the operation in the lab, then compare. A prediction that misses a line is the lab's best output (Predict the State Before Running the Code).
- When persistence arrives, do the same for save-then-load: before is the in-memory cart, the operation is a round trip, the after should be equal — and the changed list should be empty. It rarely is on the first try (The Cart Disappears).
- Keep the diff at the concept's level — entries and quantities — not at the JSON level. A serialiser reordering keys is not a state change; a lost entry is.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- add-again. Before: [ Laptop × 1 ]. Operation: add("laptop"). After: [ Laptop × 2 ]. Changed: items[0].quantity 1 → 2. Unchanged: items.length = 1; items[0].productId. The unchanged length is the rule "one entry per product" as a checked fact, and it is what the buggy always-append variant would break — its after is [ Laptop × 1, Laptop × 1 ] and its changed list says items.length 1 → 2, which nobody asked for.
- to-zero. Before: [ Laptop × 2 ]. Operation: changeQuantity("laptop", 0). After: [ ]. Changed: the Laptop entry removed; items.length 1 → 0. Writing it catches the natural mistake of writing "quantity 2 → 0": there was one entry with quantity 2, and now there are none. The number that moved is the count of entries, not the quantity; the quantity did not go to 0, the entry went away.
- invalid-zero. Before: [ ]. Operation: add("laptop", 0). After: [ ]. Changed: nothing; the output was an error. The test that came from this example asserts both halves — the throw and the deep-equality of items to [] — and the second assertion is the changed list, empty, turned into code.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every example in your worksheet has a changed list, and the lists are short.
- At least one line in an unchanged list is there because a rule demands it, and you can name the rule.
- Each changed line has a line of code next to it that caused it, and none of the code lines are missing from the list.
- The buggy variant's changed list is different from the correct one, and the difference is the bug, stated in fields.
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.
- ?For this example, what changed — field by field, old value to new — and what did not?
- ?Which line of the operation is responsible for each change, and is there a change with no line?
- ?Which unchanged field is a rule holding, and would I notice if it moved?
- ?What is the changed list for the invalid case, and does the test assert that it is empty?
What can go wrong
- Diffing the JSON rather than the state. Key order, whitespace and a
createdAtthat ticks are not state changes at the concept's level; a diff that lists them buries the one entry that vanished. - Listing only changes. The unchanged list is where invariants live; without it, "quantity set to 0 and the entry is still there" reads as a clean single-field change.
- Skipping attribution. A change with no responsible line is a change from somewhere else — a shared reference mutated by another caller — and it is the one that will not reproduce tomorrow.
- Applying it only to the cart in memory. The same three columns around a save-and-load, or around an API call and its response, is where state actually gets lost.
- Writing before / after / changed for six examples is longer than reading the six examples; the extra lines are the invariants made explicit, and they become assertions.
- The unchanged list can grow without bound — everything is unchanged except the change — so it has to be limited to fields a rule protects, which is a judgement.
- At the JSON level a diff is automatic; at the concept level it is written by a person. The manual version is slower and is the one that catches the entry that vanished.
- "So I need a diff tool." A diff tool shows differences in text; this device asks for differences in state, attributed to lines and checked against rules. The tool is useful once you know what to look for in its output.
- "If the after is correct, what changed is obvious." The after [ Laptop × 2 ] is correct; whether it got there by an increase or by an append-then-merge is not visible in the after, and only the changed list distinguishes them.
- "This is only for learning." Before / operation / after is the shape of a test assertion, of a migration review, and of an incident timeline; the cart is where it is small enough to learn on.
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.
- GENERALBefore / operation / after with a named changed list applies to any mutation — in-memory, database, API, UI state — because it is the shape of a state change rather than a property of the cart.
- SIMPLIFIEDThe cart's state is small enough to list completely; for a large aggregate the changed and unchanged lists are limited to the fields the operation's rules mention.
- ILLUSTRATIVELaptop × 1 and Laptop × 2 are the concept's own examples; the buggy variant is the concept's predict-the-bug case, chosen because its changed list is the bug.
Where the depth lives
This domain asks the question and hands the answer off by name.