What Can Happen to It?
The cart's state is items of { productId, quantity }. What can happen to it? Add an item, remove one, change a quantity, view the items, calculate the total, clear it. Six sentences of behaviour, in words, before a single function signature.
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 have the state. How do you find the operations — everything that can happen to a cart — without starting to code and discovering them one bug at a time?
The Cart interface exists: items, each with a product id and a quantity. You write addItem because it is obviously first, and then you stop. Is there a removeItem? An updateItem? A setQuantity? Does the cart need a getTotal or does the page do that? You start typing method names to see which ones feel right.
Ask AI or a tutorial for "the methods a cart class should have". The answer is a list — add, remove, update, clear, getTotal, getItemCount, applyCoupon, checkout — and it looks complete, so you paste the signatures and start filling them in top to bottom.
The list is somebody else's cart's list. applyCoupon and checkout are on it because the tutorial had them; getItemCount is on it because the badge in the header needed it. You cannot tell which of these your cart needs, because the list came from a screen and not from your state.
- The list is somebody else's cart's list.
applyCouponandcheckoutare on it because the tutorial had them;getItemCountis on it because the badge in the header needed it. You cannot tell which of these your cart needs, because the list came from a screen and not from your state. - Signatures were written before behaviour.
updateItem(item)— update what? To what? What if the item is not there? Each signature is a promise whose content you will discover while implementing it, which is exactly the order the method is meant to reverse. - The operations that came from bugs are missing: "add the same product twice" is not on the list, and it is the operation whose behaviour matters most. A list generated from method names never contains the awkward cases.
- Filling in signatures top to bottom felt like implementing a cart; it was implementing a list of names.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Ask the question in words: what can happen to a cart? Answer with sentences a shopper would recognise — "put a product in it", "take one out", "change how many", "look at what is in it", "find out what it costs", "empty it" — and only then give each one a name. The names are for the code; the sentences are for deciding what the code does.
- Derive the candidates from the state, not from a screen. Every field that can change implies an operation that changes it (items → add, remove, clear; quantity → change quantity), and every value someone needs implies a read (items → view; items + prices → total). If a proposed operation touches no field and reads nothing, it belongs to another concept.
- For each sentence, write the normal case as an example before naming anything:
[]→ add Laptop →[Laptop × 1]. The example is the behaviour; the function will be its encoding (Examples Before Algorithms). - Stop at the concept's boundary. "Check out" reads the cart and creates an order — it is an operation on orders that consumes a cart, not an operation on the cart. "Apply a coupon" changes a price, which the cart does not own. Naming the boundary is part of finding the operations (CRUD and Domain Actions).
From "what can happen?" to a question with an answer
The vague form of the question produces a method list from memory. The best form ties every operation to a field and an example, so the list can be checked against the state rather than against a tutorial.
why The best form is answerable from the state list alone and produces the parameters as a side effect; the vague form can only be answered by copying, and the middle form finds the behaviour but not the boundary.
The operations, by what they do to the state
The behaviour, decomposed by effect rather than by screen. The leaves are the record's normal cases, which is the test of an operation that has been found rather than named: it has an example.
- ├Change the items— the collection can grow and shrink
- └Add itemtestable Empty cart, add Laptop → [Laptop × 1]; Laptop already present, add Laptop → [Laptop × 2], not two entries.
- └Remove itemtestable [Laptop × 2, Mouse × 1], remove Mouse → [Laptop × 2]; removing the last item gives an empty cart, which is still valid.
- └Clear carttestable Two entries → none; clearing an empty cart is a no-op that still succeeds.
- ├Change an entry— quantity is the only mutable field inside an entry
- └Change quantitytestable Laptop × 2, set to 3 → Laptop × 3; set to 0 → the entry disappears; set to −1 → rejected.
- ├Read the cart— the state is only useful if something reads it
- └View itemstestable A cart with two entries returns two entries; an empty cart returns an empty list, not null.
- └Calculate totaltestable Laptop × 2 at 1000, Mouse × 1 at 20 → 2020; empty cart → 0.
Checkout and coupons are not in the tree: one creates an order from the cart, the other changes a price the catalog owns. Both consume the cart; neither is an operation on it.
Which operation to describe first
The behaviour list is unordered; the work of describing each one is not. The order below starts with the operation whose examples decide the most, and it is one order among several.
- 1Add item, including the repeat-add example
because Its examples decide the entry shape and the rule "one entry per product"; every later operation depends on what an entry is.
- 2View items
because Establishes what the cart hands back — entries with ids, names looked up elsewhere — before any other read is designed.
- 3Change quantity, including the zero case
because Reveals that quantity 0 is not a state and that change-to-zero is a removal; removes the temptation to store 0.
- 4Remove item, including the absent case
because Forces the error-or-no-op decision, which the API will inherit.
- 5Calculate total
because Introduces the catalog dependency and the derived-price decision, now that the entries are settled.
- 6Clear cart
because Trivial once the others exist; described last so it is not used to dodge the harder cases.
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 behaviour list with a verb and a shopper's noun in each line, no parameters, no types. If a line needs a parameter to make sense ("change the quantity — of what?"), the parameter is discovered, not guessed.
- Walk the state list and ask, per field, "what changes this?" and "who reads this?" Unanswered fields are either dropped (nobody reads them) or reveal an operation you missed.
- Add the read operations explicitly. View and total are easy to forget because they change nothing, and they are where derived values (Derived vs Stored) get their justification.
- Write one example per operation before naming it, in the record's notation. The example decides the parameters — remove needs a product id; clear needs nothing.
- Use the CRUD and Domain Actions lab: it asks for the behaviour first and refuses to show signatures until each operation has been described. The refusal is the method.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The record's six operations, as behaviour: Add item — "check the product exists, check the quantity is positive, look for an existing entry, increase it or create one." Remove item — "find the entry; if there, take it out; if not, decide: error or nothing." Change quantity — "find the entry; zero removes it; negative is rejected; otherwise set it." View items — "return the entries as they are." Calculate total — "for each entry, look up the price and add price × quantity." Clear cart — "replace the items with an empty collection."
- Derived from the state:
itemscan grow (add), shrink (remove, clear) and be read (view, total);items[].quantitycan be changed (change quantity). Six operations, all accounted for by two fields; nothing on the list touches a field the cart does not have, and nothing the cart has is untouched. - Rejected as cart operations: "apply coupon" (changes a price the catalog owns), "check out" (creates an order; the cart is its input), "get item count" (a read the view can do from the entries). Each was on the tutorial's list; each belongs somewhere else or nowhere.
How you know it worked
What now exists that did not before, and what question you can now ask.
- A list of sentences exists that a non-programmer would confirm as "what a cart does", and every sentence has an example next to it.
- Every field on the state list is changed by at least one operation and read by at least one, or has been dropped.
- Naming the functions is now trivial, and the parameters fell out of the examples.
- You can say why "checkout" is not on the list — and where it went.
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.
- ?What can happen to this thing, in sentences a user of it would recognise?
- ?For each field, what changes it and who reads it?
- ?Which proposed operations touch state this concept does not own — and which concept do they belong to?
- ?What is the example for each operation, and which parameters did the example reveal?
What can go wrong
- The list grows to cover every screen. A badge count, a "recently added" panel and a mini-cart each get an operation, and the cart becomes a view model. Reads that are trivially derived from view do not need their own operation.
- The behaviour sentences are written and then the signatures are copied from the tutorial anyway, so
updateItem(item)survives with the behaviour of change quantity bolted on. - The awkward cases are left for implementation. "Add the same product twice" belongs on the list now, because it decides what add means; deferring it is deferring the rule.
- The boundary is drawn too tightly: total is left off because "the page can do it", and the pure logic that should be tested once is reimplemented in every renderer.
- Writing behaviour before signatures delays the first compiled function by a page of sentences; on a concept with three obvious operations, the page confirmed what you knew.
- Drawing the boundary early means some operations (checkout, coupons) are deferred to concepts that do not exist yet, and the cart cannot be demoed "end to end" until they do.
- Deriving operations from fields finds what changes the state and can miss cross-cutting behaviour — merging two carts at login is not implied by any single field and arrives from a requirement (Inject a Constraint and Follow It Through).
- "Operations are the methods of the class." They are the behaviour; whether they end up as methods, free functions or endpoints is a representation decision. The record's implementation uses free functions over a plain object.
- "More operations means a more complete cart." A cart with six operations that each have examples is complete; one with twelve signatures and no examples is a list of names.
- "The reads are not really operations." View and total have inputs, outputs and errors (a product with no price) like any other; leaving them out is why derived values go unjustified.
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.
- GENERALDeriving operations from "what changes each field, who reads it" works for any stateful concept — a message can be sent, edited, deleted, read; a job can be enqueued, claimed, completed, failed — and the boundary question is the same.
- ILLUSTRATIVEThe tutorial's twelve-method list and the shopper's sentences are invented; the six operations and their plain-English steps are the shopping-cart record's.
Where the depth lives
This domain asks the question and hands the answer off by name.