What Information Changes Over Time?
Entities say what exists; state says what about them changes. Order status, cart contents, stock levels and payment status all change, each on a different trigger, and each one is a place where the system must know "the current value" — which is where most bugs live.
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 entities. Which of their attributes change over time, who changes them, and why does the answer matter more than the schema?
I have a data model — Product, Cart, Order, Payment — and it felt finished. Then someone asked whether an order can be cancelled after it has shipped, and I realised my Order has a status column and I have never written down what values it can hold or who is allowed to change it.
Add a status string column to every table that seems to need one and move on. It is flexible, it will hold whatever the code puts in it, and the values can be sorted out as features are built. The schema looks complete, and complete feels like designed.
The column holds whatever any code path wrote to it, so after a month there are orders with status = "paid", "PAID" and "payment_ok", written by three handlers that each invented a value. The state exists; nobody discovered it.
- The column holds whatever any code path wrote to it, so after a month there are orders with
status = "paid","PAID"and"payment_ok", written by three handlers that each invented a value. The state exists; nobody discovered it. - The questions state answers — can this be cancelled now? has this been paid? — are answered by each screen reading the column and guessing. The admin page and the customer page disagree about the same order.
- Because "what changes?" was never asked, the things that change without a status column — stock levels, cart contents, prices — are not seen as state at all, and are updated in place with no record of what they were.
- The data model that felt finished described the store at rest. Nothing in it described the store in motion, and motion is where the requirements were hiding.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Go through each entity and ask, attribute by attribute: does this change after the entity is created? A product's name rarely does; its price does; its stock does constantly. An order's items should not; its status must. Every "yes" is state, and every piece of state gets three more questions: what makes it change, who is allowed to cause that, and who needs to read the current value.
- Separate state that has a small set of named values — an order is one of a few things — from state that is a quantity or a collection — stock is a number, a cart is a list. Named-value state wants a state machine (Finding the State Machine); quantity state wants an invariant about the number (Invariants in an Online Store); collection state wants rules about membership.
- For each piece of state, ask whether "the current value" is enough or whether the history matters. Current stock is enough for checkout; the history of stock changes is what an admin asks for when the number is wrong. That question decides whether you overwrite or append (Overwrite or Append?).
- Write the result as a table: what changes, triggered by what, changed by whom, read by whom, history needed or not. The table is the state model, and it is the input to the state machine, the invariants and the schema — not an output of any of them.
The state table for the store
This is the move's output for the running example. Each row is an attribute that changes; the columns are the four questions. The rows that matter most are the ones with more than one trigger, because those are the rows where two changes can collide, and the rows whose trigger is outside the system, because those are the rows where the state can change without a request.
| What changes | Triggered by | Allowed actor | Read by | History? |
|---|---|---|---|---|
| Product price | price change | admin | catalog, cart, checkout | yes — orders were placed at old prices |
| Product stock | restock; checkout; cancellation | admin; system; system | catalog, checkout | yes — the number will be wrong one day |
| Cart contents | add, remove, change quantity | customer | cart page, checkout | no |
| Order status | pay; provider confirms; ship; cancel; refund | customer; provider; warehouse; customer or admin; admin | customer, admin, warehouse | yes — support will ask |
| Order items | — (decided: do not change) | — | everyone | n/a — snapshot at creation |
| Payment status | provider outcome; refund | provider; admin | checkout, order page | yes — money |
Asking about change instead of about columns
The vague form asks for a schema and gets one. The best form asks about motion and gets the state model, from which the schema follows. The difference is visible on the order: the vague question yields a status column; the best one yields the fact that the provider can change an order's state from outside, which no column shape expresses.
why Only the best form produces triggers and actors, and only triggers and actors reveal that the provider and the warehouse change orders from outside — which is where the interface work and the concurrency work come from. The middle form produces a list of values with no rules between them.
What the table cannot decide yet
Discovery raises questions it does not answer. The board keeps them as questions with experiments rather than as decisions made by default — because a status column that "will hold whatever" is exactly the default that gets made when the question stays vague.
- ✓Order items are a snapshot; the order's status changes and its history is kept.
- ✓Stock has three triggers, two of them from the system itself.
- ~Prices in an order are captured at checkout, not looked up later. If this is wrong, "order total" is state that changes when prices do.
? Can an order be changed?
becomes After an order is placed, which of "add an item", "change quantity", "change address" must be supported, and is cancel-and-reorder an acceptable answer for each?
experiment Ask the founder for the last three times a customer wanted to change an order and what support did.
? Cart persistence.
becomes Does a cart need to survive the browser being closed, and if so, for an anonymous customer too?
experiment Build the cart in local storage only and note which requirements it fails — merging on login is the usual one.
? Stock going wrong.
becomes When the stock number is wrong, what will the admin want to see to find out why?
experiment Write the three lines of a stock-history view on paper and check whether the current design could produce them.
How to do it
Most important first.
- For every entity from data discovery (Entities From Requirements), mark each attribute changes / does not change after creation.
- For each thing that changes, write the trigger as an event in plain words: "customer pays", "admin restocks", "provider confirms".
- Write who is allowed to trigger it. If the answer includes "the system itself" — a timeout, a job — the state has a trigger nobody sees on a screen.
- Write who reads the current value and what they decide from it. A reader that makes a decision from state is a reader that needs the state to be right.
- Mark whether history matters. If someone will ever ask "why is it this value?", it does (Source of Truth).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The store, attribute by attribute. Product: name and description change rarely (admin); price changes (admin) and its history matters because orders were placed at old prices; stock changes constantly (admin restocks, checkout decrements, cancellation increments) and its history matters because the number will be wrong one day. Cart: contents change (customer) and history does not matter. Order: items do not change after creation — a decision, and an important one; status changes (customer pays, provider confirms, warehouse ships, customer cancels) and history matters because support will ask. Payment: status changes (provider) and history matters because money.
- The table produced something the schema did not: order status has four different triggerers, one of which is outside the system, and stock has three, one of which is a cancellation that most people forget. Both are places where two changes can arrive at once, and the state model is where that gets noticed before it is a bug (Invariants Under Concurrency).
- The chat app, same move. Message: text does not change (or does it? "edit message" is a requirement someone will ask for — write it down as a decision); delivered and read states change, triggered by the recipient's client, which is outside. Conversation: members change; last-read position per member changes constantly and is read by every client to draw the unread badge. That last one is the state with the most readers and the most frequent writes, and the table found it in a minute.
How you know it worked
What now exists that did not before, and what question you can now ask.
- A table exists with a row per changing attribute, and every row has a trigger, an allowed actor and a reader.
- At least one trigger is outside the system or is the system itself, and you can point to where that is handled.
- The things you decided do not change — order items, message text — are written down as decisions, not assumed.
- You can say for each piece of state whether it is overwritten or appended, and why.
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.
- ?Which attributes of this entity change after it is created, and what event causes each change?
- ?Who is allowed to cause each change — a user, an admin, an external system, a timer?
- ?Who reads the current value and what do they decide from it?
- ?For this piece of state, will anyone ever ask why it has the value it has?
What can go wrong
- Every attribute is treated as state with a history. The product description gets an audit log; the state model is longer than the requirements. The move is to find what changes in ways that matter, not to journal everything.
- The trigger is written but the actor is not, so "status changes to paid" is in the table and any code path is allowed to do it. The actor column is what the state machine will need.
- The table is made from the schema instead of from the requirements, so it only contains attributes that already have columns and misses "who has seen this message", which has no column yet.
- State found is state left as strings. The table says order status has four values and the column still accepts any text; the discovery did not reach the code (States That Must Be Unrepresentable).
- The table is a document to maintain alongside the schema, and on a small system the schema and the code are the table — the discipline is what costs, not the artefact.
- Deciding that order items do not change closes a door: editing an order becomes cancel-and-reorder. It is the right decision for most stores and a real cost for some.
- Keeping history where it matters means more rows, more storage and a query for "current" that is no longer a single read; the ask "why is it this value?" is paid for before it is asked.
- "State is the stuff in the database." The database holds state; it is not the definition of it. A cart in the browser, a payment the provider knows about and you do not yet, and a message delivered but not read are all state, and only some of it is in your tables.
- "A status column is a state machine." A column is where a state is stored. The machine is the set of allowed transitions, and it exists only if someone wrote it down and the code enforces it.
- "If the history matters I need event sourcing." History that matters needs to be kept; an audit table next to the current row keeps it. Event sourcing is one way, with its own costs, for when the history *is* the source of truth (Event Sourcing).
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.
- GENERAL"What changes, triggered by what, by whom, read by whom" applies to any system with entities that live longer than a request — a CI pipeline's job status, a document's review state, a file's scan status.
- STAGE-SPECIFICOn a greenfield store the table is written before the schema. In an existing system it is recovered by reading every place that writes the status column, and the recovery usually finds values the original author never intended.
- ILLUSTRATIVEThe store's attributes and their triggers are invented to show the shape of the table; a real store has more state and more actors, including fraud review and returns.
Where the depth lives
This domain asks the question and hands the answer off by name.