Overwrite or Append?
For every piece of state there is a choice: keep the current value, keep the history, or both. Stock is a number that is overwritten and a history that is appended; the choice depends on who will ask "why?", and getting it wrong is cheap to fix early and expensive later.
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.
For a piece of state that changes, should the system keep only the current value, the sequence of changes, or both — and what question decides it?
Stock is a number in the product row. Checkout decrements it, restocking increments it. A customer says an item showed as in stock and then was not, the admin says the number is wrong, and I have nothing to look at: the number is what it is and I do not know how it got there.
Store the current value, because that is what the code reads. stock = 12. It is the simplest thing, every query is a single read, and when history is mentioned it sounds like the kind of feature that can be added later if anyone asks.
Someone asks. The number is wrong, and the only evidence is the number. Reconstructing how it got there means reading logs that were not written for this, and the answer is usually "we do not know".
- Someone asks. The number is wrong, and the only evidence is the number. Reconstructing how it got there means reading logs that were not written for this, and the answer is usually "we do not know".
- The current-value model makes some questions unaskable, not merely hard: "how many units did we sell last month?" cannot be answered from a stock column that has been overwritten a thousand times.
- When history is added later, it is added as a second thing that must be kept in step with the first, by every code path that writes the first. The paths that forget are the ones that produce the discrepancies.
- The opposite reflex, on a team that has been burned: journal everything, and now "current stock" is a sum over a table that grows forever, and checkout — the one reader that needs it fast — is the one that pays.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- For each piece of state, ask who will read it and what they will ask. Readers that ask "what is it now?" need the current value. Readers that ask "why is it that?" or "what was it then?" need the history. A piece of state usually has both kinds of reader, and the two kinds have opposite needs: fast single reads against a complete record.
- Decide the source of truth. Either the current value is authoritative and the history is a log of changes to it, or the history is authoritative and the current value is derived from it. The first is the normal case; the second is event sourcing, and it is the right answer when the sequence of changes *is* the business record — a ledger, a payment — rather than a convenience.
- Then decide what the history records. Not "stock was 12, then 11" — that is two snapshots — but "checkout for order O decremented by 1": the change, its cause, its actor and its time. History that records causes can answer "why?"; history that records values can only answer "when?".
- Keep the two consistent by construction, not by discipline: the change and the log entry are written in the same transaction, or the current value is a view over the log. A rule that "every writer must also log" is a rule that one writer will break.
Two representations, one transaction
The pair shows the stock decision in code. Both versions keep the current value; only one can answer "why?". The important line in the better version is not the insert — it is that the insert and the update are inside the same transaction, so there is no state of the world in which one happened and the other did not.
Checkout runs `UPDATE product SET stock = stock - qty WHERE id = ?`. Restocking runs the same with a plus. The number is always current and never explained.
In one transaction: `UPDATE product SET stock = stock - qty WHERE id = ? AND stock >= qty`, then `INSERT INTO stock_movement (product_id, delta, cause, actor, at)`. Checkout still reads one number; the admin reads the movements.
The record of a change is written by the same statement sequence that makes the change, so no code path can forget it and no crash can separate them. The stock >= qty guard is the invariant living in the write, which the movement log then explains (Enforcing Invariants).
1-- why is stock for product 42 what it is?2SELECT at, delta, cause, actor3FROM stock_movement4WHERE product_id = 425ORDER BY at DESC;6 7-- how many did we sell? (derived, not stored)8SELECT -SUM(delta) FROM stock_movement9WHERE product_id = 42 AND cause = 'checkout';Neither query was possible from a stock column alone, and neither slows down checkout, which still reads the column.
Current, history, or history-as-truth
Three options, and the criteria are the lesson. The third is event sourcing, named here as one option among three rather than as a philosophy, because the store needs it for one piece of state and not the others.
What does the system keep?
when Every reader asks "what is it now?" and nobody has a credible "why?". Cart contents; a user's presence.
cost Questions about the past are unanswerable, not merely slow. Write the decision down so the gap is known.
when Many now-readers and a few why-readers. Stock; product price. The normal case.
cost Two writes per change, kept together by a transaction. The log records causes or it is worthless.
when The history *is* the business record and every reader needs it to be complete: payments, refunds, a ledger. Or when replaying the past is a requirement.
cost "Current" is derived and must be cached or projected for fast readers; the model is unfamiliar to most teams and its tooling is its own subject (Event Sourcing).
What must be a snapshot
The same question — what changes over time? — decides which fields on an order must be copies rather than references. The decomposition lists the order's fields by that test; every leaf says how you would check the choice was right.
- ├Things that change elsewhere — snapshot them
- └Unit price at purchasetestable Change the product price after the order; the order total does not change.
- └Product name at purchasetestable Rename the product; the order page still shows the name the customer bought.
- └Shipping address at purchasetestable The customer edits their address; the shipped order goes to the old one.
- ├Things that are the order's own — keep current
- └Statustestable Status changes through allowed transitions only, and each change is recorded with its cause.
- ├Things that should stay references
- └Customer idtestable The customer changes their display name; the order page shows the new one, because identity is not a snapshot.
How to do it
Most important first.
- For each piece of state from the table (What Information Changes Over Time?), list the readers and split them: now-readers and why-readers.
- If there are why-readers, decide what the record of a change must contain: the delta or new value, the cause (which order, which admin action), the actor, the time.
- Decide which is authoritative — current value or history — and make the other derived or written in the same transaction (Source of Truth).
- For state with no why-readers today, write down that the decision was "current value only" so it is a decision and not an omission.
- Check the order's snapshot fields — price at purchase, address at purchase — are captured, not referenced, because the referenced thing changes (Snapshots vs References).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Stock. Now-readers: catalog, checkout — many reads, must be fast. Why-readers: the admin when the number is wrong; whoever reports sales. Decision: current value is authoritative and lives in the product row; every change writes a stock-movement row in the same transaction with the delta, the cause (order id or admin action), the actor and the time. Checkout reads one number; the admin reads the movements; the sales report sums the movements with cause = checkout. The overwrite and the append coexist, and neither is a discipline.
- Cart contents. Now-readers: the cart page and checkout. Why-readers: none anyone has asked for. Decision: current value only, written down as a decision so that if "abandoned cart analysis" becomes a requirement, the missing history is a known gap and not a surprise.
- Payment. Now-readers: checkout and the order page. Why-readers: support, finance, the provider's dispute process. Decision: here the history is the thing — every attempt, outcome and refund is a row, and "is this order paid?" is derived from the rows. Overwriting a payment status would destroy the record that matters most when something goes wrong, so this is the one piece of store state where the append is authoritative.
How you know it worked
What now exists that did not before, and what question you can now ask.
- Every piece of state has a written answer — current, history or both — with the readers that justify it.
- Where both exist, they are written in one transaction or one is derived from the other; there is no rule that depends on every writer remembering.
- History entries record causes, and you can answer "why is stock this number?" by reading them.
- The order captures the price it was placed at, and changing the product's price does not change past orders.
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 reads this state, and do they ask "what is it now?" or "why is it that?"?
- ?If both, which is authoritative — the current value or the sequence of changes — and how is the other kept in step by construction?
- ?What must a record of a change contain for "why?" to be answerable — the cause, the actor, the time?
- ?Which fields on this entity must be snapshots of something that will change elsewhere?
What can go wrong
- Everything is appended. Cart changes, page views and price lookups get history nobody will read, the tables grow without bound, and "current" becomes an aggregation everywhere.
- History is added as a separate write with no transactional link to the current value, and the first crash between the two writes produces the discrepancy the history was meant to explain.
- The history records values and not causes — a snapshot per change — so it can say stock was eleven at noon and cannot say why.
- Event sourcing is chosen for the whole store because payment needed an append-only record. The right answer for one piece of state is applied to state that only had now-readers.
- Keeping history costs rows and a second write on every change; keeping only the current value costs the ability to answer questions nobody has asked yet. The decision is made before the questions arrive.
- Deriving the current value from history makes "is it paid?" a query over rows, and that read is on the response path. It is right for payment and wrong for stock, and the reason is the number of readers.
- Writing the change and its record in one transaction ties them to one database. When the history is meant to live elsewhere — an analytics store — that coupling is the thing that has to be broken carefully (The Transactional Outbox).
- "History is an audit log, which is a compliance feature." History is how the system explains itself. The admin who asks why stock is wrong is not auditing; they are debugging, and the log is the evidence (Logs Are Evidence, Not Thinking).
- "If I keep history I can drop the current value." You can, and then every reader that wanted the current value pays for an aggregation. Keep both when both kinds of reader exist.
- "Append-only is safer." It is more complete. Safety is about whether the two representations can disagree, and that is decided by whether they are written together, not by which one you keep.
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.
- GENERALNow-readers against why-readers is the question for any state in any system; a build system's cache, a user's permissions and a file's scan status all have both kinds.
- DOMAIN-SPECIFICMoney makes the history authoritative: a payment or a ledger is its sequence of changes, and overwriting is destroying evidence. For a cart or a presence indicator the current value is all anyone wants, and history is cost with no reader.
- ILLUSTRATIVEThe stock-movement table and the reader lists are invented to show the shape of the decision; a real inventory system has reservations, returns and adjustments that add rows and causes.
Where the depth lives
This domain asks the question and hands the answer off by name.