StateGENERALSTAGE-SPECIFICILLUSTRATIVE

What Must It Remember?

A cart is "a temporary collection of products the user intends to buy". Before a class or a table, ask what that collection has to remember — items, an owner, possibly a currency; for each item a product id and a quantity — and write the candidates down as a list you can challenge, not as code.

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.

The question

You know you need a shopping cart and you know what it means. What information does the cart have to hold, and how do you find out without writing code?

The situation

The concept is defined: a temporary collection of products the user intends to purchase. You open the editor to "make the Cart class" and freeze at the first line, because you do not know what goes inside it. Product name? Price? A total? A date? Every tutorial cart you have seen has a different set, and none of them said why.

The reflex

Look at a tutorial's cart model and copy its fields. It has id, userId, items with name, price, quantity, image, subtotal, total, createdAt, updatedAt. Twelve fields, typed, in a language you know. It compiles, and the class looks like something a real store would have.

Why it stalls

The class has twelve fields and no operations, so nothing can happen to it yet — and nothing in the fields says what the cart does. It is a shape with no behaviour, and the first time you try to write addItem you discover you do not know which of the twelve fields it changes.

What the reflex produces — and fails to produce
  • The class has twelve fields and no operations, so nothing can happen to it yet — and nothing in the fields says what the cart does. It is a shape with no behaviour, and the first time you try to write addItem you discover you do not know which of the twelve fields it changes.
  • Every field arrived with someone else's reason. total is there because the tutorial rendered it; name because the tutorial had no catalog. You cannot say which fields your cart needs because you never asked the question the fields answer.
  • The state was never challenged, so it will be challenged later by bugs: the stored total drifts from the items, the stored name goes stale when a product is renamed, and each bug is fixed by adding a sync step rather than by removing the field that caused it.
  • Motion was mistaken for progress: a typed model exists and the cart is exactly as unimplemented as before, because the model came from a screen and not from the meaning.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Start from the meaning, not from a screen. "A temporary collection of products the user intends to purchase" names two things directly: a collection, and products in it. Everything the cart remembers has to justify itself against that sentence.
  • Write the candidates as a plain list, one line each, with the reason each one might be needed. Do not type them into a class yet; a list can be argued with and a class invites you to defend it.
  • Split the list into the container and the entries. A cart remembers items and, once there is more than one shopper, an owner; a cart item remembers which product and how many. Everything else — name, price, total, currency, timestamps — goes on the list with a question mark until Challenging Unnecessary State answers it.
  • For each candidate, ask the two questions that decide state: does any operation read it, and can it be worked out from something already there? A field no operation reads is not state; a field that can be computed is derived (Derived vs Stored).

The state board, before any class

The Unknowns Board works for state as well as for requirements. The known column is what the meaning sentence gives for free; the unknowns are the fields whose place is not yet earned, each turned into a question with the experiment that answers it — and for state, the experiment is usually "name the operation that reads it".

Nothing on the board is typed and nothing is a column. That is deliberate: a field on a board can be dropped in a sentence; a field in a migration cannot.

The cart's state, as a board
known
  • The cart is a collection — so it has items.
  • Each item points at a product — so it has a product id; the catalog owns the rest.
  • Intent has an amount — so an item has a quantity.
assumed
  • ~One currency in V1. Written down so the currency field arrives with the requirement, not with a guess.
  • ~One shopper in V0. The moment there are two, owner stops being optional.
unknown → question → experiment
  1. ? The cart needs the product name.

    becomes Does any cart operation read the name, or is it only the renderer that needs it — and can the renderer ask the catalog?

    experiment Write getItems in plain English and see whether the name appears anywhere in the cart's own logic. It does not; verdict derive.

  2. ? The cart needs a total.

    becomes Can the total be computed from items and catalog prices every time it is needed, and what does storing it cost on every change?

    experiment Write total as a loop; count the operations that would have to update a stored total (add, remove, change quantity, clear, price change). Verdict drop.

  3. ? The cart needs a price per item.

    becomes Is the price a property of the cart or of the product, and what should the shopper see when the price changes while the cart is open?

    experiment Change a catalog price with an item in the cart and decide what the cart should show. The current price — so derive; the order will snapshot.

The board is the state canvas before it is a canvas: every row has a verdict or a question that leads to one.

Container and entries

Once the candidates are listed, they fall into two levels: what the cart as a whole remembers, and what one entry remembers. Keeping the levels apart is what makes "one entry per product" sayable — it is a rule about the entries, and the container has no opinion.

The leaves say how you would know the field is right, because a field without an observable consequence is a field no operation reads.

What the cart remembers
Cart state
  • The containerwhat the cart as a whole holds
    • itemstestable An empty cart holds an empty collection, not null; after add Laptop it holds exactly one entry.
    • owner (depends)testable With two shoppers, each finds their own cart by owner and never the other's; in V0 the field is absent and nothing breaks.
    • currency (depends)testable Absent in V1; the single-currency assumption is written in the record so its removal is a known change.
  • One entrythe smallest thing the rules talk about
    • productIdtestable getItems returns ids the catalog can resolve; an id the catalog does not know was rejected at add.
    • quantitytestable Adding Laptop twice gives one entry with quantity 2, and no entry ever shows quantity 0.
  • Not statederived or dropped, with the reason recorded
    • productName, price, total, createdAttestable Renaming a product in the catalog changes what the cart shows with no cart write; the total is right after every operation without a sync step.

Three kept, three deferred with triggers, four derived or dropped. The V1 cart is two fields per entry and one collection.

The canvas as it leaves this step

What this step hands to What Can Happen to It? is a text canvas, not a class. It fits on half a page and every line can be defended. The type column is deliberately loose — "money", "id" — because how money is represented is a later decision and a wrong early choice (a float) is one of the classic ways a total goes wrong.

The cart's state canvas, V1
1Cart
2 items collection of CartItem keep the cart is its items
3 owner user id or session id depends absent in V0; required with two shoppers or persistence
4 currency code depends one currency assumed in V1; field arrives when that changes
5 total money drop computed from items + catalog prices; a stored copy needs syncing
6 createdAt timestamp drop no V1 operation reads it
7
8CartItem
9 productId id keep the catalog owns the product; the cart points at it
10 quantity integer > 0 keep "one entry per product" needs a count to hold
11 productName string derive loaded from the catalog; storing it goes stale on rename
12 price money depends derived for a cart; snapshotted for an order

Read the verdict column, not the name column. A canvas whose every verdict is keep has not challenged anything.

The implementation ladder

Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.

concept Shopping Cart beginner
Build it step by step →

Shopping Cart = A temporary collection of products the user intends to purchase, held between browsing and checkout.

Identity, ownership, lifetime
  • 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.
State it must remember
  • 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.
Operations
  • 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
Rules that must always hold
  • 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 meaning sentence at the top of the page and derive the first fields from its nouns. "Collection" gives items; "products" gives a reference per item; "intends to purchase" hints at quantity, because intent has an amount.
  • List every field you are tempted to add, including the ones you suspect are wrong. The list is for challenging; a field left off the list because it felt wrong will come back without a reason attached.
  • Mark each field with one of four verdicts — keep, derive, drop, depends — and write the tradeoff next to any verdict that is not keep. The shopping-cart record does exactly this and it is the artefact the What Information Changes Over Time? lab produces.
  • Do not name a type yet beyond the obvious ("id", "integer > 0", "money"). Types are a representation decision and arrive with Representation Mapping; a field's existence is a meaning decision and arrives now.
  • Stop when every operation you can name (What Can Happen to It?) has the state it needs and no field is unread. That is the test of a finished state list, not the number of fields.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • The cart record's container fields: items — "the cart is its items; without them nothing else means anything" — verdict keep. owner (user id or session id) — keep once there are two shoppers or any persistence; verdict depends, because a single in-memory demo has one cart and no owner. currency — depends: one currency in V1 is an assumption worth writing down, and the field appears when the assumption changes.
  • The entry fields: items[].productId — "the catalog owns the product; the cart only points at it" — keep. items[].quantity (integer > 0) — keep, because two laptops is one entry with quantity 2 and the rule "one entry per product" needs a quantity to hold.
  • The candidates that did not survive: items[].productName — derive, from the catalog. items[].price — depends: derived for a cart, snapshotted for an order. total — drop, it is a short loop over the items. createdAt — drop, no V1 operation reads it. The list had nine candidates; the V1 cart keeps three of them and defers three.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • A page exists that lists every candidate field with a verdict and, for anything that is not keep, the reason — and none of it is code yet.
  • You can point at each kept field and name the operation that reads it and the operation that changes it.
  • The question "should I store the total?" has a written answer with a tradeoff, instead of a stored total that will need syncing.
  • The state list is short enough that the first operation is obvious to write, because there are only two things to change.

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.

Next questions
  • ?Which nouns in the meaning sentence become fields, and which fields have no noun to justify them?
  • ?For each candidate, which operation reads it — and if none does, why is it here?
  • ?Which fields belong to the container and which to the entries, and what is one entry?
  • ?Which verdicts are depends, and what exactly would flip them to keep?

What can go wrong

How the move itself fails
  • The list becomes a schema. Types, nullability and indexes are decided for fields that have not yet earned their place; the challenge step is skipped because the fields already look committed.
  • The challenge is applied only to the fields that felt suspicious, and items itself is never asked "what is an item?" — so the entry shape (id + quantity) is assumed rather than discovered from examples (State Shape From Examples).
  • Fields marked depends are treated as "later" and then forgotten. owner marked depends must come with the trigger that flips it — two shoppers, or persistence — or it will be missing exactly when the trigger arrives.
  • The move is run on a concept whose meaning was never written down, and the fields are derived from a vague feeling of what a cart is. State follows meaning; if Define the Concept was skipped, go back.
What the move costs
  • A challenged state list is shorter than a copied one, and every field you dropped is a field you may add back when a requirement arrives — with a reason, this time, but also with a migration if the cart is already persisted.
  • Writing the list before the class delays the moment something compiles. On a concept you have modelled many times, the list is in your head and the delay is wasted.
  • Deriving instead of storing trades a synchronisation bug for a lookup cost; the cart is small enough that the lookup is invisible, and the tradeoff is written down so it can be revisited when it is not.
Misreads
  • "Fewer fields is always better." Fewer *unjustified* fields is better. An order that dropped price because the cart did would lose the amount the customer paid — the verdict is per concept, not a preference for minimalism.
  • "This is database design." It is the step before database design. The same list becomes a TypeScript interface, a JSON body and two tables, and each of those adds decisions of its own (Representation Mapping).
  • "Depends means undecided." Depends means decided-with-a-trigger: owner is absent in V0 and required in V1, and the record says which requirement makes the difference.

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 fields from the meaning and challenging each against "which operation reads it?" applies to any concept — a message, a booking, a job — not only to a cart; the fields differ, the two questions do not.
  • STAGE-SPECIFICOn a greenfield concept the list is invented; in an existing system the list is read from the current model and the move becomes marking which existing fields no operation reads any more — usually several.
  • ILLUSTRATIVEThe nine candidate fields, the laptop and the single-currency assumption are the shopping-cart concept record's worked example; a real store would have its own list and its own verdicts.

Where the depth lives

This domain asks the question and hands the answer off by name.

Further
  • /manifesto/delegating — the tutorial's field list is delegated understanding; the verdict column is yours