Systems Thinking
Change a product's price and the change reaches the cart, checkout, order history and refunds. Following one fact through every component that reads it reveals where the fact should be captured — and it is rarely where the reflex puts it.
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 product's price changes. Which parts of the store are affected, and how do you find them before a customer does?
The admin wants to be able to change a price. It looks like an edit form and an UPDATE. But last week a refund came out wrong after a sale ended, and nobody could say which part of the code had decided the amount — so I suspect "just a price edit" is touching more than the products table, and I do not know how to find out what.
Implement the edit. A price is a column on Product; the admin form writes it; the catalog reads it. That is a complete, testable feature, and it can be done before lunch. Anything else that shows a price presumably reads the same column, so it will update itself.
The edit ships and the catalog is right. The cart, which stored a price when the item was added, now shows a different total on the checkout page than on the cart page — and neither number was written by the code you changed, so the bug report lands on someone else's desk.
- The edit ships and the catalog is right. The cart, which stored a price when the item was added, now shows a different total on the checkout page than on the cart page — and neither number was written by the code you changed, so the bug report lands on someone else's desk.
- Order history was reading the current product price to render old orders. After the change, customers see that they "paid" today's price for last month's order. The change was correct everywhere it was made and wrong everywhere it was not.
- Refunds compute from whatever they can find, which is now the new price. The feature that was "an UPDATE" has silently altered how much money leaves the company, and no test failed, because no test knew that refunds and prices were connected.
- Each of these arrives as a separate bug, weeks apart, and each is fixed locally. Nobody draws the path the price takes, so the same class of bug returns with the next fact that changes — a product name, a tax rate, a shipping cost.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Pick the fact that is changing — here, "the price of product P" — and trace it as a thing that *travels*, not a column. Ask: where is it written, and where is it read? Every reader is a place the change arrives, and every reader that copies the fact is a place where two versions can disagree.
- For each reader, ask what it *needs*: the price now, or the price at some moment in the past? The catalog needs now. The order needs the moment of purchase. The cart is the interesting case, because the answer is a product decision, not a technical one.
- The readers and their needs tell you where the fact must be captured. A fact that only ever needs "now" is read from its owner; a fact that some reader needs "as it was then" must be copied at that moment, deliberately, by the code that owns the moment — and the copy is a different fact with a different name.
- Do this before the edit form, because the edit form is the easy part. The systems move is the map of readers; the feature is whatever the map says it is. Often it is "the UPDATE, plus a snapshot on OrderItem that already should have existed".
Where a price goes
The reflex sees a column. The move sees a fact with an owner and a set of readers, and asks of each arrow which version of the fact travels along it. The diagram is the store after the map was drawn: the product row owns the live price; checkout copies it at the instant an order is created; everything downstream of the order reads the copy.
The arrows labelled "copies at checkout" are the feature. Before the map they did not exist, and the readers on the right were reading the live value through the dotted path — correctly, until the first price change.
Every reader, sorted by which version it needs
The matrix is the working artefact behind the diagram, and it is the one to produce first because it is a list, not a picture. The "needs" column is a product decision in disguise — the cart row was settled by asking the founder, not by reading code — and the "reads today" column is what the grep found.
Anywhere the two right-hand columns disagree is a bug that has not happened yet.
| Reader | Needs | Reads today | Should read |
|---|---|---|---|
| Catalog and product page | now | Product.price | Product.price |
| Cart line and cart total | now, and say if it changed | CartItem.price copied at add | Product.price, compare with copy |
| Checkout total | now, at the instant of the order | Product.price | Product.price, then capture |
| Order history | then — moment of purchase | Product.price | OrderItem.unitPrice |
| Invoice email | then | Product.price | OrderItem.unitPrice |
| Refund calculation | then — what was paid | Product.price | OrderItem.unitPrice |
What the map turns into
The map only pays off if it changes what gets built. Here it turns a one-line feature into a four-part one, and — more usefully — it turns "the totals are wrong" from a search into a question with a short answer. The order below is one way to ship it; the alternative is the one to take when orders already exist and the snapshot has to be back-filled before anything reads it.
- 1Add OrderItem.unitPrice and capture it in checkout
because Nothing else can be fixed until the captured value exists; new orders start being correct immediately.
- 2Point history, invoice and refund at OrderItem
because The three "then" readers are the money-bearing bugs; moving them is the actual feature.
- 3Write the invariants as tests
because "An order's total never changes after placement" fails against the old code and passes now — the map becomes something the build checks.
- 4Ship the admin edit form
because The reflex's whole feature is last, because it is the only part that was never risky.
How to do it
Most important first.
- Name the fact in plain words and list every screen, job, report and endpoint that displays or uses it. Grep for the column name and for the words humans use for it; the second list is usually longer.
- For each reader, write "now" or "then" beside it. "Then" readers need a captured value; note the moment it should be captured and which code owns that moment (Snapshots vs References).
- Draw the propagation as a diagram with the owner in the middle and arrows to the readers, labelled "reads live" or "copies at X". Anywhere two arrows land on the same screen with different labels is a place the screen can contradict itself.
- Turn each "then" into an invariant you can test: "an order's displayed total never changes after it is placed" (Invariants in an Online Store). Then build the edit form.
- Repeat for the next fact that can change — stock, name, tax — and notice the map is mostly the same. That reuse is the point of drawing it.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Fact: price of product P. Writers: the admin edit form. Readers: catalog page (now), product page (now), cart line (now — decided with the founder: the cart shows the current price and warns if it changed since the item was added), checkout total (now, at the instant the order is created), order history (then), invoice email (then), refund calculation (then — the amount paid, not the amount listed).
- The map shows three "then" readers that were all reading "now". The feature becomes: the UPDATE on Product, a
unitPricecaptured on OrderItem when checkout creates the order, and order history, invoice and refund reading from OrderItem instead of Product. The edit form is unchanged; the other three edits are the actual feature. - Invariants written down from the map: the total shown on an order never changes after placement; a refund never exceeds what the order item was paid for; the cart total shown on the checkout page equals the total the order is created with. Each becomes a test that fails before the OrderItem snapshot exists and passes after.
- The same map, reused for "admin changes stock": readers are the product page (now), the cart (now, with a "no longer available" case), checkout (now, at the moment of the order — the concurrency question in Invariants Under Concurrency), and nothing "then". A different fact, the same move, a much smaller feature.
How you know it worked
What now exists that did not before, and what question you can now ask.
- You can name every place a fact is read, and for each say whether it wants the live value or a captured one — and the "then" readers have a named field they read from.
- The feature estimate changed after the map. If "change a price" is still "an UPDATE and a form" after the map, either the store is simpler than most or the map was not finished.
- A bug report about a mismatched total now has an obvious first question — which reader, live or captured? — instead of a search.
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 fact is changing, and where is every place it is read?
- ?For each reader, does it want the value now or the value as it was at some moment — and which moment?
- ?Where is the fact captured today, and where does the map say it should be?
- ?Which two screens could show different numbers for the same thing after this change, and what would the customer conclude?
- ?What is the next fact that can change, and how much of this map does it reuse?
What can go wrong
- The map is drawn for every fact in the system before any feature is built. Systems thinking is applied per change, at the moment of change; a whole-system propagation map is a documentation project that is out of date when it is finished.
- Every reader is made a "then" reader, so everything is snapshotted and nothing updates. The cart that froze the price at add-time is the classic case: a customer returns after a sale ends and expects the sale price, and the store now has to argue with them.
- The trace stops at the code boundary. The invoice PDF, the accounting export and the email the provider sends all read the price too; the map has to leave the repository to be complete.
- The move is used to justify a rewrite: "everything is coupled to price, so we need an event bus". The map found readers; it did not find a bottleneck. See Change Propagation for what the coupling actually costs before proposing a mechanism.
- Tracing readers takes longer than making the edit; on a fact with one reader it is wasted. The map earns its time on facts that money, law or customer trust depend on.
- Captured values are duplicated data, and duplicated data can diverge. Each snapshot has to be justified by a reader that needs "then", or it is a second source of truth waiting to be wrong.
- A propagation map is a claim about the whole system made by one person on one day. It is only as good as the grep and the conversation that produced it.
- "So every fact should be denormalised into the places that use it." No — only facts a reader needs as-of a moment. The catalog reading Product.price directly is correct; copying it there would create the divergence the move exists to prevent.
- "Systems thinking means drawing an architecture diagram." The diagram in this lesson has arrows labelled with *which version* of a fact flows along them. A boxes-and-lines picture of services says nothing about whether the refund reads the live price.
- "This is what event sourcing is for." Event sourcing is one way to reconstruct "then"; a captured column is another and far cheaper. The map says which readers need history, not which mechanism provides it.
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.
- GENERALAny fact with more than one reader propagates — a price, a user's display name, a file's permissions, an ML feature definition. The move is identical; the "now versus then" question is what changes per fact.
- DOMAIN-SPECIFICIn a store, the "then" readers are the ones money and law depend on — orders, invoices, refunds — and the map is worth an afternoon. In an internal dashboard where every screen shows the live value, the map is a five-minute grep and the move is nearly free.
- ILLUSTRATIVEThe store, the sale that ended and the wrong refund are invented to show the shape of propagation; no real incident is described, and the reader list is a plausible one rather than a complete one.
Where the depth lives
This domain asks the question and hands the answer off by name.