Derived vs Stored
The total is computed, not stored: a stored total is a second source of truth that has to be kept in sync on every change. The price is derived for a cart and snapshotted for an order — and that difference is the difference between a cart and an order.
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.
Which values should the concept compute when asked, and which must it hold on to — and how do you tell, for a value like price, that the answer changes with the concept?
The cart shows a total on every screen. You add a total field, update it in addItem, and the number is right. Then removeItem forgets to update it, then changeQuantity updates it with the old price, and now the cart says 2040 for two laptops and a mouse that should cost 2020. You start writing a recalculateTotal() that every operation must remember to call.
Store the total and be disciplined about updating it. It is faster to read, every screen needs it, and "just call recalculate at the end of every operation" is a rule the team can follow. A stored, indexed total is what a real system would have.
The discipline is the bug. A stored total is correct only if every code path that changes items also changes the total — and the next operation someone adds (clear, merge at login, a price change in the catalog) is a code path they will not remember. The number drifts, and it drifts silently.
- The discipline is the bug. A stored total is correct only if every code path that changes items also changes the total — and the next operation someone adds (clear, merge at login, a price change in the catalog) is a code path they will not remember. The number drifts, and it drifts silently.
- The recalculate call produces the appearance of safety: the total is recomputed and then stored, so there are still two sources of truth, one of which is wrong between the mutation and the call.
- The fix that was never considered is the one the record chose: do not store it. A total is a loop over the items; the loop is a few lines and is right by construction.
- The same reasoning was not applied to price, so it was either stored everywhere (stale carts) or derived everywhere (orders whose totals change after payment). The cart-versus-order distinction was never noticed.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Sort every value into three kinds: base state, which nothing else can produce (items, quantity); derived values, which are a function of base state and other owners (total, name, cart price); and snapshots — derived values deliberately frozen at a moment because the moment matters (order price, order total).
- Derived values are not stored unless a measurement says the computation is too expensive, and even then they are stored as a cache with an explicit invalidation rule, not as state. The record's verdict on
totalis drop: "a stored total is a second source of truth that has to be kept in sync with the items on every change; computing it is a short loop." - Snapshots are stored on purpose and never recomputed. The question that separates a snapshot from a derived value is: if the source changes, should this change too? For a cart the answer is yes — the shopper should see the current price. For an order it is no — the price the customer paid must not change afterwards.
- That last question is the boundary between the cart and the order. They hold the same items; the cart derives, the order snapshots; and checkout is the operation that turns one into the other by copying prices at that moment. Write this down as a rule, because it is one (Rules Determine Implementation).
The rule that makes total a loop
A derived value is a rule about consistency: the total *is* the sum of price × quantity over the items, at all times. Storing it turns a definition into an obligation. The rule device shows the definition, the check that would catch a drifted stored total, and the code that makes the check unnecessary.
rule The total always equals the sum over items of price(productId) × quantity — and is never negative.
↓ becomes validation If a total is stored, assert after every operation that it equals the recomputed sum. If it is derived, there is nothing to assert: the loop is the definition. Non-negativity follows from positive quantities and non-negative catalog prices; the check belongs on the price, in the catalog.
function total(cart, priceOf):
sum = 0
for each item in cart.items:
sum = sum + priceOf(item.productId) * item.quantity
return sum
-- no stored field, no recalculate() to forgetEvery value, sorted
The three kinds side by side, with the record's verdict for each. The interesting column is the last one: what happens when the source changes. Base state has no source; derived values follow it; snapshots ignore it on purpose.
| Value | Kind | Verdict in the record | When the source changes |
|---|---|---|---|
| items, productId, quantity | base | keep | There is no source; these are the source. |
| productName (cart) | derived | derive | The cart shows the new name on the next render; no cart write. |
| price (cart) | derived | depends → derive while a cart | The shopper sees the current price — intended. |
| total (cart) | derived | drop | Recomputed by the loop; nothing to sync. |
| price (order) | snapshot | depends → snapshot once an order | Unchanged — the price paid must not move. |
| total (order) | snapshot | stored at checkout | Unchanged; recomputing it from live prices would be the bug. |
What goes wrong when the kind is wrong
Each row is a value given the wrong kind. The responses are all the same move: decide who owns the value and whether this concept should follow the owner or freeze it.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| removeItem changes items but not the stored total | The cart shows 2040 for items that sum to 2020; refresh does not fix it. | A derived value was stored and one code path forgot the sync. | Drop the field; compute total from items on every access. |
| A catalog price rises after an order was paid | The order page shows a total higher than the customer paid; support tickets. | The order derives price from the live catalog instead of snapshotting at checkout. | Copy prices onto order items at checkout; never recompute an order from the catalog (Snapshots vs References). |
| A product is renamed | Open carts show the old name; new carts the new one. | The cart stored a copy of a value the catalog owns. | Derive the name at render; the catalog is the owner. |
| A report over every open cart is measured slow | The dashboard times out computing totals. | Derivation at a scale where it is no longer free. | Introduce a named cache with an invalidation rule — and only now, with the measurement in hand. |
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 candidate value, write the function that would compute it from base state. If you can write it in a few lines and it depends on nothing unavailable, the default is derive.
- Ask "if the source changes, should this change too?" A yes is derived; a no is a snapshot; a "sometimes" means two concepts are hiding in one — usually a temporary one and a permanent one.
- Name the moment a snapshot is taken and the operation that takes it. For the store it is checkout: "an order captures the cart's items and prices at that moment" is the leaf you already wrote in the store decomposition.
- Only store a derived value after measuring, and when you do, call it a cache in the name and write its invalidation rule beside it.
cachedTotalwith "recomputed on every item change" is honest;totalis a trap. - Test derived values by mutating the source: rename a product, change a price, remove an item — and check the derived value without calling any sync. If it is wrong, it was stored somewhere.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The record's
total: "Every screen shows the total" was the reason to keep it; the challenge — "can the total be computed from the items and their prices every time it is needed?" — gave the verdict drop, with the trade-off "drop it until a measured reason says otherwise." The implementation istotal(cart, priceOf): start at zero, add price × quantity for each item. - The record's
items[].price, verdict depends: "For a cart, derive the price from the catalog so the shopper always sees the current price; for an order, snapshot it, because the price the customer paid must not change afterwards. The difference is the difference between a cart and an order." - The failure mode the record lists for getting this wrong: "The price changes between adding and checking out: the cart shows the new price because it derives it; the order must snapshot it." A store that derives on the order shows a customer a receipt that disagrees with their bank statement.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every value on the state list is labelled base, derived or snapshot, and each derived one has the function that computes it.
- The concept has no field whose correctness depends on someone remembering to update it.
- Checkout is described as the operation that turns derived prices into snapshotted ones, and the order model has fields the cart model does not.
- A price change in the catalog changes what every open cart shows and changes no existing order.
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.
- ?Can this value be computed from base state in a few lines — and if so, why would I store it?
- ?If the source changes, should this value change too?
- ?Which operation takes the snapshot, and at what exact moment?
- ?Which of my concepts is temporary and which is permanent — and are two of them hiding in one model?
What can go wrong
- Everything is derived, including the things that must not move. An order that derives its total from live prices is the textbook failure, and it happens because "derive" was heard as a principle rather than a verdict.
- Derived values are stored "for performance" before any measurement, and the sync bugs arrive without the performance ever having been a problem.
- The snapshot moment is left implicit. If nobody says "at checkout", the order is created from a cart whose prices were read at add time, at render time or at payment time depending on the code path.
- A cache is introduced and called state. The name matters: a field called
totalwill be trusted; a field calledcachedTotalwith an invalidation rule will be checked.
- Deriving costs a computation on every access. For a cart total it is a loop over a handful of items; for a value over a large collection it may need to become a measured cache.
- Snapshotting costs storage and a copy step at a defined moment, and it means the order model has fields the cart model does not — two models where a lazy design would have one.
- The base/derived/snapshot sort is another pass over the state list, and on a concept with three fields it is over in a minute — the value is in the concepts where it is not.
- "Storing the total is a premature-optimisation mistake." Storing it *without a measurement* is. The slogan "premature optimisation is the root of all evil" is falsifiable here: it is wrong when a report over every cart is measured to be slow and a maintained aggregate is the fix. The mistake is the missing measurement and the missing invalidation rule, not the storing.
- "Snapshots are duplicated data and therefore bad." A snapshot is the record of a fact — what was paid — and it is the only correct representation of that fact. Duplication is bad when the copy is supposed to track the source; a snapshot is supposed not to.
- "Derived values do not need tests." They are where the source-change bugs show up; test them by changing the source.
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.
- GENERALThe base / derived / snapshot sort applies to any concept with computed values — message counts, account balances, booking prices; the cart total is the smallest example of it.
- DOMAIN-SPECIFICIn a store, the cart derives and the order snapshots; in a ledger every balance is derived from entries and nothing is snapshotted except period closes; in analytics almost everything is a stored aggregate with a documented refresh. The question is the same and the verdicts differ by domain.
- ILLUSTRATIVEThe totals 2020 and 2040, the laptop at 1000 and the mouse at 20 are the shopping-cart record's numbers, chosen for the shape of the argument.
Where the depth lives
This domain asks the question and hands the answer off by name.