Follow the Data
Pick one value — an order, a price, a cart — and follow it from where it enters to where it rests, through every transformation and hand-off. Code is organised by module; behaviour is organised by data, and following the data reads the code in the order it actually runs.
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 need to know what happens to a piece of data between the request and the database, and the code is spread across a dozen files. How do you read it in the order it runs?
The order total on the confirmation page is sometimes different from the total the customer saw in the cart. The cart, the checkout, the pricing and the order modules each do something with a price, and you do not know which of them is wrong or in what order they run.
Read the pricing module. It has "price" in the name, and a discrepancy in prices must come from it.
The pricing module computes a price correctly from its inputs; the discrepancy is in which inputs it was given, and that is decided elsewhere. Reading it thoroughly finds nothing wrong, which is true and useless.
- The pricing module computes a price correctly from its inputs; the discrepancy is in which inputs it was given, and that is decided elsewhere. Reading it thoroughly finds nothing wrong, which is true and useless.
- Modules are read one at a time, and the hand-offs between them — where a price is copied, rounded, converted, or replaced — are never seen, because they live in the calling code, not in either module.
- Order of execution is guessed from the folder layout. In reality the cart snapshots the price, the checkout recomputes it, and the order stores whichever it was given last; none of this is visible without following a single price through.
- The fix, when guessed, is applied to the module that was read, and the discrepancy moves rather than closes.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Choose one concrete value and follow it. Not "prices" — the price of one product, in one cart, for one checkout. Start where it enters the system and read forward, function by function, in call order: what receives it, what it is transformed into, what it is stored as, what is read back, what is shown.
- At each hand-off, ask three things. What shape is the value in here? What does the next function assume about it? Is it copied, referenced, recomputed or replaced? Most data bugs are at hand-offs — a snapshot taken at the wrong moment, a recomputation with different inputs, a reference to something that has since changed (Snapshots vs References).
- Write the path down as a list of stations with the value's state at each. The list is the trace; the discrepancy is between two adjacent stations, and now you know which two.
- Following the data is also how Reading a Codebase's "one feature end to end" is actually done, and it is the reading move that a vertical slice is the building move for: the same layers, traversed in the same order, by one value.
One price, through the layers
The trace is the reading form of a vertical slice: the same layers a slice would build, traversed by one value. What it proves is where the value went; what it does not prove is that where it went was right — that is the design question the trace hands you.
- Catalog (products.price)Holds the current price; changes when admin edits it.
- Cart add (cart_items.price_snapshot)Copies the current price at add time — a snapshot.
- Cart pageDisplays the snapshot.
- Checkout buildOrderLinesRecomputes from products.price — ignores the snapshot.
- Order (order_items.unit_price)Stores whatever checkout computed.
- Confirmation pageDisplays the stored order price.
The trace as pseudocode
Written as stations, the trace fits on a screen and reads in execution order regardless of how many files the code spans. The annotations — copy, reference, recompute — are the part that matters; they are where the design decisions hide.
1product p1 price = 19.99 (catalog; mutable by admin)2 → cart.add(p1) price_snapshot = 19.99 (COPY at add time)3 → cart page shows 19.99 (reads snapshot)4 [admin sets p1.price = 17.99]5 → checkout.buildLines unit = products.price (RECOMPUTE — reads catalog, not snapshot)6 → order_items unit_price = 17.99 (stores recomputed value)7 → confirmation page shows 17.99 (reads order)8 9disagreement: between "cart page" and "checkout.buildLines"10cause: two different answers to "which price is authoritative", one per moduleEvery arrow is a hand-off and every hand-off is labelled with what happened to the value. The bug is at the one labelled RECOMPUTE that disagrees with the earlier COPY; nothing else on the path needed reading.
Module-first against data-first
The same question read two ways. The module reading is thorough and finds nothing, because nothing in any single module is wrong. The data reading is narrow and finds the hand-off, because the hand-off is the only place the two modules' assumptions meet.
Open `pricing/`, read every function, confirm each computes correctly from its arguments, conclude pricing is fine, open `checkout/` and repeat, conclude checkout is fine, and have two correct modules and one wrong total.
Start at `products.price` for p1, follow the calls: cart add copies it, checkout recomputes it from the catalog. The two stations disagree after an admin edit. One function read, one design question surfaced.
The bug is not inside either module; it is in the fact that two modules answered "which price?" differently. Module reading cannot see a disagreement between modules; following the value passes through both and sees the hand-off where they diverge.
How to do it
Most important first.
- Pick a real instance: order 4412, product p1, listed at one price in the catalog. Concrete values make hand-offs visible; abstract "prices" do not (Example-Driven Thinking).
- Find where it enters: the catalog read, the cart add. Read forward in call order, not file order — follow the function calls, open files as they are called.
- At each station record: the variable or column, its value or shape, and whether it was copied, referenced or recomputed from something.
- Find where it rests — the
order_items.unit_pricecolumn — and where it is read back for display. - Compare adjacent stations until two disagree. Read the code between them. That is the reading the bug needed; the rest of the modules were context.
Worked on a concrete problem
The move has to produce something. This is what it produced.
- Product p1, catalog price 19.99. Station 1:
products.price= 19.99. Station 2: cart add copies it intocart_items.price_snapshot= 19.99 (a snapshot, at add time). Station 3: cart page displays the snapshot: 19.99. Station 4: admin changes the product price to 17.99 that afternoon. Station 5: checkout'sbuildOrderLinesrecomputes fromproducts.price= 17.99 — a recomputation, ignoring the snapshot. Station 6:order_items.unit_price= 17.99. Station 7: confirmation shows 17.99. Stations 3 and 5 disagree, and the code between them is one function that reads the product instead of the cart item. - The finding is not "checkout is wrong". It is a design question the trace exposed: should the customer pay the price they saw when they added the item, or the price at checkout? Both are defensible; the store had implemented both, in different places, without deciding. The fix follows the decision, and the trace is what made the decision visible (Source of Truth).
- A second trace on the chat app: one message, from the send button to the recipient's screen. Stations: client state, the POST body, the server's parsed message, the
messagesrow, the realtime event payload, the recipient's client state, the rendered bubble. The bug — messages occasionally appearing twice — turns out to be between the last two: the client appends the event and also re-fetches, and both paths add the message. Following one message found it; reading the messages module would not have.
How you know it worked
What now exists that did not before, and what question you can now ask.
- You have a list of stations for one value, with its shape and state at each, and you can point at the code for every transition.
- The discrepancy is located between two adjacent stations, and the code between them is short.
- You can say, for each hand-off, whether the value was copied, referenced or recomputed — and whether that was a decision or an accident.
- The modules you did not read are the ones the value never passed through.
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 one concrete value should I follow, and where does it enter the system?
- ?At this hand-off, what shape is the value in, what does the next function assume, and is it copied, referenced or recomputed?
- ?Where does the value rest, and where is it read back and shown?
- ?Between which two adjacent stations does the value first disagree with what it should be?
- ?Where the value is recomputed, was that a decision, and is it the same decision the earlier snapshot made?
What can go wrong
- Following an abstract value. "Prices" pass through everything; the price of p1 in cart 42 passes through seven stations. Concreteness is what makes the trace finite.
- Following in file order. Opening
pricing.tsbecause it is next in the tree, rather than because the last function called into it, loses the order of execution the trace exists to recover. - Stopping at the database. The value is read back and displayed, and display is a station too — formatting, rounding and currency conversion have all hidden bugs at the last step.
- Tracing a value that is not the one in the report. The bug is in the total; the trace follows a unit price and misses the quantity multiplication where the actual error was.
- A trace covers one path. A value that branches — a price used in tax, discount and total calculations — needs a trace per branch, and the effort grows with the fan-out.
- Following in call order through a framework means crossing framework code — middleware, ORM hooks, serialisers — that transforms the value invisibly; the trace has to include them or it has gaps exactly where the surprises are.
- The trace answers where the value went; it does not by itself answer whether that was right. The design question that follows is separate work.
- "Follow the data means read the data model." The model says what can be stored; the trace says what happened to one value on its way there. Both are needed; they are different readings.
- "The bug is where the value is wrong." The value is first wrong at a station; the cause is the transition into it, and the transition can be a recomputation whose inputs were changed three stations earlier.
- "A debugger does this for me." A debugger shows one station at a time, in detail. The trace is the list of stations, and it is what tells you where to put the breakpoint.
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 value in any system has a path from entry to rest; the stations differ — a form field, a message, a file, a metric — and the move of following one concrete instance in call order does not.
- DOMAIN-SPECIFICIn a store, money is the value to follow and snapshot-versus-recompute is the recurring question; in the chat app it is a message and the question is delivery once; in an analytics dashboard it is a metric and the question is which aggregation ran when.
- ILLUSTRATIVEThe prices, the seven stations and the double-appended chat message are invented to show a trace locating a discrepancy; no real pricing logic is described.
Where the depth lives
This domain asks the question and hands the answer off by name.
- — A Testing & Reliability domain would put the trace into a test — add, change the price, check out, assert the charged price — which is the characterization test for the decision once it is made.