Challenging Unnecessary State
Do you need to store the product name inside the cart, or can it be loaded from the catalog? Every candidate field gets that question, and the answer is a trade-off — duplication and staleness against a lookup per render — written down, not a preference.
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 field is on the list and it would be convenient to have. How do you decide whether the cart should store it, and what does the wrong decision cost?
Your cart item has a product name and a price next to the quantity, because the cart page shows them and it seemed odd to store an id and nothing readable. A month later marketing renames "Laptop" to "Laptop Pro" and every open cart still says Laptop. Someone suggests a nightly job to resync cart names with the catalog.
Keep the field and add the sync. The name is already there, the page already reads it, and a background job that refreshes cart names is a contained piece of work that fixes the visible bug. It feels like hardening the system.
The sync fixes the symptom and keeps the cause: the cart now has two sources of truth for the name, and the job is the tax on keeping them agreed. The next duplicated field — price — will need its own job, and the one after that.
- The sync fixes the symptom and keeps the cause: the cart now has two sources of truth for the name, and the job is the tax on keeping them agreed. The next duplicated field — price — will need its own job, and the one after that.
- Nothing in the sync answers the question that was never asked: why did the cart store the name at all? The stored field had a reason ("convenient to render") that was never weighed against its cost, so the cost arrived as a surprise.
- The pattern generalises badly. A cart that stores whatever the page shows becomes a copy of the catalog with a quantity column, and every catalog change becomes a cart bug.
- Work was done — a job, a schedule, a log line — and the concept is no better understood. Motion, not progress.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Ask of every candidate field: is this a property of the cart, or of something the cart points at? A name is a property of the product. The cart holds a reference to a product; holding a copy of the product's properties is duplication, and duplication has to be kept in sync by someone.
- Make the trade-off explicit in both directions. Storing the name: no lookup at render, goes stale on rename. Deriving it: a lookup per render, always current. Neither is free; the decision is which cost you would rather pay, and for a cart the lookup is cheap and the staleness is visible.
- Find the owner. Whichever concept is the source of truth for the value — the catalog for names and prices, inventory for stock — keeps it; everyone else derives it from the owner. Ownership is the general form of the question, and it is why the same field gets different verdicts in different concepts (Derived vs Stored).
- Write the verdict and the trade-off next to the field. The record's entry for
productNameis one line: derive — the catalog is the source of truth for names. That line is what stops the nightly job from being proposed.
The why ladder under "store the name"
The claim "the cart needs the product name" sounds like a requirement and is a convenience. Walking it down finds the requirement underneath — the page needs to show a name — and a simpler way to meet it that leaves the cart honest about what it owns.
The ladder also says when the claim is right. A cart that must render offline, or a store that has measured the catalog lookup as its bottleneck, has a reason to store the name — and a reason means a staleness policy comes with it.
“The cart item should store the product name.”
- ↓Why store the name? So the cart page can show "Laptop × 2" without another call.
- ↓Why is another call a problem? It was assumed to be slow, and it seemed odd to hold an id with nothing readable next to it.
- ↓Is it slow? Unmeasured. A cart holds a handful of items; the catalog lookup is one indexed read per item, or one batched read for all of them.
- ↓What happens to the stored name when the product is renamed? It stays. Every open cart shows the old name until something rewrites it — and nothing does.
the claim was right when The cart must render with no catalog reachable (an offline-first client), or the catalog lookup has been measured as the bottleneck on the cart page — and in either case the stored name comes with a written refresh rule.
Both costs, side by side
A trade-off is only a trade-off if both sides have a cost. The comparison keeps the stored version honest — it does have an advantage — and says why the derived version wins for a cart specifically.
CartItem { productId, quantity, productName }. Render reads the name from the item. A rename in the catalog leaves the cart showing the old name until a sync job runs — and the job is now part of the system.CartItem { productId, quantity }. Render asks the catalog for the name by id. A rename is visible on the next render with no cart write and no job.The catalog is the source of truth for names, so the cart cannot be correct about a name on its own; it can only be a copy that is right until the owner changes it. Deriving removes the copy and the sync that keeps it right, at the price of one cheap lookup where the name is actually needed.
The same question, different answers
The challenge is not "delete fields". It is a decision with options, and the record shows all four verdicts coming out of the same question. Which one applies depends on who owns the value and whether this concept is temporary.
This field would be convenient to have on the cart. Should it be state?
when The cart is the owner of the value — items, productId, quantity. Nobody else can be asked for it.
cost The cart is responsible for keeping it valid: the rules in Rules Determine Implementation are about these fields.
when Another concept owns it and this one is temporary — productName, and price while it is still a cart.
cost A lookup wherever the value is needed, and a dependency on the owner being reachable.
when It is a function of fields already present, or no operation reads it — total, createdAt.
cost Recomputation on every access; a migration if a requirement later needs it stored.
when The value is the cart's own but only under a condition not yet met — owner with two shoppers, currency with a second currency.
cost The trigger must be written down, or the field is missing when the condition arrives.
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 each field, name the concept that owns the value. If it is not this one, the default verdict is derive, and the field stays only if you can name a reason strong enough to accept the sync cost.
- Say what goes stale. "If the catalog changes X, the cart shows the old X until Y." If there is no Y, the field is a bug waiting for a rename.
- Say what the lookup costs in the place where it happens. One catalog read per rendered item, on a cart of a handful of items, is nothing; the same lookup in a report over every open cart is a different question with a different answer.
- Check whether the convenience belongs to the renderer rather than the cart. The cart page needs names; the cart does not. The renderer can ask the catalog, which is exactly what getItems in the record assumes.
- Use the What Information Changes Over Time? lab: tick the fields you believe you need and read the challenge Atlas raises for each. The point is not to agree with it but to be able to answer it.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The record's challenge for
items[].productName: "Do you need to store the product name inside the cart, or can it be loaded from the product catalog?" Trade-off, verbatim: "Storing it duplicates the catalog and goes stale when a product is renamed; loading it costs a lookup per render. Derive it — the catalog is the source of truth for names." - The same question asked of
items[].pricedoes not get the same answer. The catalog owns prices, so a cart derives them — the shopper sees the current price. But the moment the cart becomes an order, the price the customer paid must not change afterwards, so the order snapshots it. Same field, two owners over its lifetime, two verdicts. - Asked of
owner: the value is the cart's own — nobody else is the source of truth for whose cart this is — so it is not a derive question at all. It is a depends question: needed when there are two shoppers or any persistence, absent in a one-cart demo.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every field that is a copy of another concept's property has been named as such, and either removed or kept with a written reason and a written staleness rule.
- You can say, for each derived field, where the lookup happens and roughly what it costs there.
- The nightly-sync proposal has a one-line rebuttal: the cart does not own names.
- The same question — who owns this value? — has started to answer questions about other concepts: orders, invoices, notifications.
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.
- ?Who owns this value — and if it is not this concept, what happens when the owner changes it?
- ?What goes stale if I store it, and who notices?
- ?What does the lookup cost in the place where it actually happens?
- ?Is this concept temporary or permanent — and does that flip derive to snapshot?
What can go wrong
- Every field is derived on principle, including the ones the concept owns. A cart that derives its quantity from "the number of times the user clicked add" has confused its own state with an event log.
- The lookup cost is dismissed everywhere because it was negligible on the cart page. Derivation has a place where it flips — the report over ten thousand carts — and the verdict should say so rather than pretend derivation is free.
- The snapshot case is forgotten: an order that derives its prices from the live catalog shows customers a different total from the one they paid. Deriving is right for the cart precisely because the cart is temporary; permanence changes the answer.
- The challenge is done once and never re-run. A field that was derive in V1 may become keep when caching is measured to be necessary — the verdict is a decision with a trigger, not a law.
- Deriving costs a dependency: the cart can no longer be rendered without a catalog, which is fine in a store and awkward in a unit test that now needs a fake catalog.
- A derived value can disappear — a product removed from the catalog while it sits in a cart leaves an id that resolves to nothing, and the cart has to decide what to show.
- The written trade-off takes longer than the tutorial's copy did; on a field nobody will ever rename, the minute spent was a minute lost.
- "Never duplicate data." Orders duplicate prices on purpose; caches duplicate everything on purpose. The rule is: duplicate with an owner, a staleness policy and a reason — not never.
- "Derive means compute on every access." It means the source of truth lives elsewhere. Whether the value is fetched, cached or memoised is a performance decision made later, when there is a measurement.
- "The challenge is about saving storage." A product name is a few bytes; the cost is correctness under change, not space. Storage has almost never been the reason.
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.
- GENERALOwnership decides the verdict for any duplicated value in any concept; the cart's product name is the shape of the question, not the only instance of it.
- SCALE-SPECIFICA lookup per render is invisible on a cart page and measurable in a report over every open cart; at that scale the verdict can flip to a cached copy with an explicit refresh — the decision changes, the question does not.
- CONTESTEDPractitioners who favour document-shaped models argue that embedding the name and price in the cart item is correct: the cart is a self-contained document, reads are the common path, and a rename of a product mid-session is rare enough that showing the old name is acceptable or even desirable. That view is strongest where the store denormalises for read performance and accepts staleness as a documented policy.
- ILLUSTRATIVEThe rename to "Laptop Pro", the nightly sync and the report over ten thousand carts are invented to show where the trade-off bites; the field list is the shopping-cart record's.
Where the depth lives
This domain asks the question and hands the answer off by name.