"The Cart Should Sync Across Devices"
One sentence from the product owner and three assumptions the cart was built on stop being true: one device, one writer, the browser as the truth. Training for requirement change is learning to hear the sentence as a list of assumptions and to find, in the worksheet, every artefact that encoded them.
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 new requirement arrives in one sentence. Which assumptions in your worksheet does it break, which artefacts encoded those assumptions, and how do you find them before the change finds you?
Your cart survives a reload — V2, browser storage, done last week. Today's message: "shoppers want their cart on their phone too". You think "so I need a database", open a terminal, and stop, because you are not sure whether that is the change or a consequence of it.
Jump to the solution. "Sync across devices" means a server, so add a database, an endpoint and a login, and wire the browser to it. It is a well-known shape and there are tutorials for every piece of it.
The pieces are right and the order is wrong. Building the database first produces a cart table with no answer to "what happens to the cart in localStorage when the user logs in on the laptop?" — the merge rule — because the assumption it breaks was never named, so the artefact that needs it was never found.
- The pieces are right and the order is wrong. Building the database first produces a cart table with no answer to "what happens to the cart in localStorage when the user logs in on the laptop?" — the merge rule — because the assumption it breaks was never named, so the artefact that needs it was never found.
- Three assumptions broke and one was addressed. "One device" got the server; "one writer" — two tabs, or a phone and a laptop, adding at once — and "the browser is the truth" — the local copy versus the server's — were not on the list because there was no list.
- The tutorials each solve their piece and none of them knows about the cart's rules. The unique constraint on (cart_id, product_id) — the rule "one entry per product" moving into the database — is not in the "add an endpoint" tutorial, and it is the line that makes the sync safe.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Hear the sentence as assumptions, not as a solution. Write the worksheet's assumption register — the ones the cart was built on — and read the requirement against each. "Sync across devices" contradicts: the cart lives in one browser (V2's persistence choice); there is one writer at a time (every in-memory operation assumed it); the local copy is the truth (the frontend held the cart). Three broken assumptions, one sentence (The Assumption Register).
- For each broken assumption, find the artefacts that encoded it. The worksheet is organised so this is a search, not an archaeology: persistence encoded "one browser"; the operations' lack of any owner or version encoded "one writer"; the frontend section encoded "local is truth". Each hit is a change; the list of hits is the scope of the work, and it is longer than "add a database" and shorter than "rewrite".
- Then decide the new assumptions, explicitly. The server's cart is the truth and the browser's is a cache; concurrent adds are resolved by the database constraint plus a last-write or a merge; the anonymous cart merges at login by a named rule. Each is a line in the register, and each is what the next requirement will break (Dangerous Assumptions).
- Only then the pieces, in the loop's order for a modification: rules (the unique constraint; the merge rule), examples (two devices add the same product), operations (the API), persistence (the rows), frontend (the cache and the rollback on a rejected add). The database is the fourth step, and by then its schema is dictated by rules already written (Understanding Is Demonstrated by Modification).
One sentence, three assumptions, seven artefacts
The decomposition takes the requirement apart along the assumptions it breaks, and under each assumption lists the worksheet artefacts that encoded it. Every leaf is testable, because every leaf is a change with an example that would show it done. The root is the sentence; nothing under it is a technology.
- ├Breaks: the cart lives in one browser— V2 chose browser storage
- └Persistence moves to a shared storetestable Add Laptop on the laptop, open the phone: [ Laptop × 1 ] appears without any local copy having existed.
- └Load-on-start becomes a fetchtestable A fresh browser with empty localStorage shows the server's cart on first render.
- ├Breaks: the browser holds the truth— the frontend section said the UI holds the cart in state
- └The server's cart is the truth; the UI copy is a cachetestable After every operation the UI replaces its copy with the response; a stale tab shows the server's state after its next action.
- └A rejected add rolls the cache backtestable Add past the limit on a stale tab: the UI shows the server's rejection and the previous quantity, not the optimistic one.
- └An API exists for the five operationstestable POST /cart/items with { productId: "laptop" } returns the cart with Laptop × 1; the same request again returns Laptop × 2.
- ├Breaks: one writer at a time— every operation assumed nobody else was mutating
- └"One entry per product" becomes a database constrainttestable Two concurrent inserts for (cart, laptop): one succeeds, the other is rejected by the constraint rather than creating a second row.
- └Increase is atomictestable Phone and laptop each add Laptop to [ Laptop × 1 ] at the same moment → [ Laptop × 3 ], never [ Laptop × 2 ].
Seven leaves, three sections of the worksheet. "Add a database" is the artefact that implements the first and third branches; it is not the root, and it does not cover the second.
The order the change is made in
The pieces, in the loop's order for a modification. The database is fourth. The alternative order — API first — is valid and the device says when you would take it; what is not valid is any order in which the rules and examples come after the schema, because then the constraint is an afterthought instead of a rule.
- 1Write the new rules and examples
because The unique constraint and the atomic increment are rules; the two-devices example is what proves them. Both exist before any technology is chosen, so the schema is dictated rather than guessed.
- 2Write the new assumptions into the register
because Server is truth, browser is cache, concurrent adds resolved by constraint plus increment; the next requirement will be read against these lines.
- 3Add the API for the five operations
because The operations already exist and are tested; the endpoints wrap them. This step changes no behaviour, which is why it comes before persistence changes any.
- 4Move persistence to the database, with the constraint
because The rows representation is in the concept, and the constraint is the rule from step one; the schema has no decisions left to make.
- 5Turn the frontend's state into a cache with rollback
because This is the last assumption to change and the one the user sees; it is safe to do only once the server is authoritative and rejects correctly.
The race, as before / operation / after
The failure mode that "one writer" was hiding, written as a state change with two operations at once. The wrong after is what the in-code rule produces under two writers; the right after is what the constraint and the atomic increment produce. The changed list names the difference, which is the reason the rule moved into the database (Invariants Under Concurrency).
server cart: [ { productId: "laptop", quantity: 1 } ]with the constraint and an atomic increment: [ { productId: "laptop", quantity: 3 } ]. With the in-code rule only: [ Laptop × 2 ] (one add lost) or [ Laptop × 1, Laptop × 1, Laptop × 1 ] (both found nothing and appended).The slice that proves the first assumption is gone
The first thing to build is the thinnest path that shows a cart added on one device appearing on another — one operation, every layer. It proves the "one device" assumption is dead. It deliberately does not prove the other two, and the device says so, because a passing slice that is read as "sync works" is how the race reaches production.
- FrontendThe add button calls POST /cart/items and replaces its local copy with the response.
- APIPOST /cart/items calls the existing addItem over the cart loaded for this owner and returns it.
- Persistencecart_item rows with the unique (cart_id, product_id) constraint; addItem's result is saved before the response.
- Second browserGET /cart on load shows [ Laptop × 1 ] with no localStorage involved.
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.
- Keep an assumption register in the worksheet from V0 on — "one shopper", "one process", "one device", "one writer", "local is truth" — each with the version that introduced it (Making Assumptions Explicit).
- When a requirement arrives, read it against the register before reading it against the code. Write the list of broken assumptions; if the list is empty, the requirement is a new rule, and the previous lesson applies.
- For each broken assumption, grep the worksheet, not the code: which section encoded it? The worksheet's sections are the artefact list — meaning, state, operations, invariants, examples, structure, pseudocode, implementation, tests, persistence, API, frontend, failure modes.
- Write the examples for the new failure modes before choosing any technology: phone adds Laptop while laptop adds Laptop; login with an anonymous cart that overlaps the owned one.
- Write the new assumptions into the register and date them. The next requirement — "sync offline edits" — will break "the server is reachable", and it should find the line that says so (Requirements Emerge During Implementation).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The register before the requirement, from the concept's versions: V0 one shopper, one process; V1 several carts, still one process; V2 one device, the browser holds the cart, one writer. "Sync across devices" read against it: one device — broken; browser holds the cart — broken; one writer — broken, because a phone and a laptop are two.
- Artefacts encoding each: one device → persistence (localStorage) and load-on-start. Browser holds the cart → the frontend section ("the UI holds the cart in state"), and the absence of any API. One writer → every operation mutates in place with no version or owner check; the rule "one entry per product" lives only in code, which two writers can both pass at once. Seven hits across three sections. None of them is "install a database".
- New examples: [ Laptop × 1 ] on the server; phone adds Laptop and laptop adds Laptop at the same moment → [ Laptop × 3 ], not [ Laptop × 2 ] and not two rows — which needs the database constraint and an atomic increment, and is the concept's first failure mode. Anonymous [ Mouse × 1 ] merged at login into owned [ Laptop × 2 ] → [ Laptop × 2, Mouse × 1 ]; with an overlap, the merge rule decides. Two examples, both written before a schema exists.
- New register lines: the server is the truth and the browser a cache (V3); concurrent adds are resolved by the constraint and an atomic increment; anonymous carts merge at login by summing quantities (V4). The database arrives as the artefact that encodes the first two — and its schema is the concept's rows representation, with the unique constraint the rule asked for.
How you know it worked
What now exists that did not before, and what question you can now ask.
- The requirement was written as a list of broken assumptions before it was written as a list of tasks.
- Every broken assumption has the worksheet sections that encoded it next to it, and the work list is those sections.
- The examples for the new failure modes — two devices, one product — exist before the schema does.
- The register has new lines with a version on them, and you can say which future sentence would break each.
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 lines in the assumption register does this sentence contradict?
- ?Which sections of the worksheet encoded each of those assumptions — and is the work list just those sections?
- ?What are the examples for the new failure modes, before any technology is named?
- ?What are the new assumptions, and which future sentence would break each of them?
What can go wrong
- Reading the requirement as a technology. "Sync" heard as "database" skips the register, addresses one assumption of three, and finds the other two in production.
- Keeping the register in your head. It works for V0 and V1; by V3 the assumptions are in five sections of a worksheet and a memory of them is a guess.
- Treating every requirement as breaking assumptions. Maximum quantity breaks none — it adds a rule — and forcing it through the register produces an empty list and a wasted step; the empty list is itself the answer, and it takes a minute.
- Writing the new assumptions nowhere. The change is made, the server is the truth, and nobody wrote it down; the next requirement — offline edits — contradicts it silently and the frontend's cache becomes a second truth by accident.
- A register maintained from V0 is a few lines per version that mostly never get read; it is read once, on the day the requirement lands, and that reading is what it is for.
- Finding artefacts through the worksheet rather than the code depends on the worksheet being current; a stale worksheet sends you to sections that no longer encode anything.
- Deciding the new assumptions explicitly is slower than letting the tutorial's defaults decide them, and it is the only version in which the merge rule is a choice rather than a surprise.
- "So the answer to sync is always a database." The answer to "one device" is a shared store; the answer to "one writer" is a constraint and an atomic increment; the answer to "local is truth" is a cache with a rollback. A database is where two of those happen to live. The register produced them; the database did not.
- "Requirement change training means predicting requirements." It means reading the ones that arrive as assumption breaks fast, not guessing which will arrive. The concept's versions are a plausible sequence, and the register makes each step cheap, whichever order they come in.
- "The assumption register is the same as the requirements list." Requirements say what must be true; assumptions say what was taken to be true without being asked for. "One device" was never a requirement; it was the shape of V2, and that is why nobody saw it until it broke.
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 a requirement as a list of broken assumptions, then finding the artefacts that encoded them, applies to any concept and any sentence — the cart's sync is the running example because its register is short enough to show whole.
- SCALE-SPECIFICA tiny store can accept "last write wins" for two devices and skip the atomic increment; the moment a lost add costs a sale, the concurrency links are where the resolution moves, and the register should say which choice was made and why.
- ILLUSTRATIVELaptop × 3, the seven hits and the phone-and-laptop race are for the shape of the argument; the versions and the failure mode are quoted from the concept record.
Where the depth lives
This domain asks the question and hands the answer off by name.