Source of Truth
The current price of a product and the price a customer paid are two different facts with two different owners. Naming the source of truth for each fact — and noticing when one fact is actually two — is how the same number stops appearing in two versions.
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 each fact the system shows, which single place is authoritative — and is "the price" one fact or two?
The cart says one total, the confirmation email says another, and the admin order view says a third. Each was computed from a price the code found somewhere. I have been asked "which one is right?" and I realise the system has no answer, because nobody ever said which place *is* the price.
Pick the database. Product.price is the real price, the others are copies, so make everything read Product.price and the three numbers will agree. It is a clear rule and it removes the duplicates.
The three numbers now agree — and the confirmation email for last month's order shows this month's price. The rule "one place is authoritative" was right; the assumption that there was one fact was wrong, and the fix turned a display inconsistency into a false record of what was paid.
- The three numbers now agree — and the confirmation email for last month's order shows this month's price. The rule "one place is authoritative" was right; the assumption that there was one fact was wrong, and the fix turned a display inconsistency into a false record of what was paid.
- The payment provider has its own record of what was charged, and it disagrees with Product.price as soon as a discount or a currency rounding is involved. Now the database is "authoritative" for a number it did not actually charge.
- Because "source of truth" was decided once, globally, it is never revisited per fact. Inventory follows the same rule and reads the live count everywhere, including in the order that was placed when the count was different.
- Every future disagreement is settled by which system is "the" source of truth, which becomes a political question between teams, rather than by asking which fact each reader actually wanted.
The move
Precisely enough to apply it to a problem you have never seen — not a slogan.
- Start from the reader's question, not from the storage. "What is this product's price?" and "what did this customer pay?" sound like one question with one answer; they are two facts. The first is owned by whoever can change it — the product record. The second is owned by whoever witnessed it — the order, at the moment it was created, and behind it the payment provider's record of the charge.
- For each fact, name exactly one authoritative place, and say what makes it authoritative: it is where the fact is *decided*, not where it is most convenient to read. Everything else that holds the value is a copy, and copies are allowed only if they say what they are a copy of and when they were taken.
- When two facts have the same name, give them different names.
Product.priceandOrderItem.unitPriceare not the same field in two tables; they are different facts that happen to be equal for an instant at checkout. The rename is the design decision. - For facts decided outside your system — whether a payment succeeded, whether a parcel was delivered — the source of truth is outside, and your record is a copy with a timestamp. Design the copy to be re-derivable from the owner (Which Dependency Must Answer Before the User Can Be Told Anything?).
One word, two facts
The compare is the whole lesson in one pair. On the left, "price" is one column read from everywhere; the totals agree and the history is false. On the right, the reader's two questions have two owners with two names, and the totals are allowed to differ where they should.
Cart, checkout, order history, invoice and refund all read Product.price. Every screen agrees. Change the price and every historical order silently changes what the customer "paid".
Product.price is what the admin decides and what the catalog and cart show. OrderItem.unitPrice is what checkout witnessed and what history, invoice and refund show. They are equal at the instant of checkout and independent afterwards.
The readers were asking two different questions. A single owner can only answer one of them; the second answer needs a witness at a moment, which is what the captured column is.
The ownership table for the store
The table is what the move produces. The "decided by" column is the test for the owner: the admin decides the list price, checkout decides the order total, the provider decides whether a charge happened, and each of those is where the fact lives. Everything in the last column is a copy and says so.
| Fact (the reader's question) | Decided by | Owner | Copies, and what they know |
|---|---|---|---|
| Current list price of product P | the admin | Product.price | catalog cache (invalidated on edit) |
| Unit price charged on order O for P | checkout, at creation | OrderItem.unitPrice | invoice PDF (rendered from the order, re-renderable) |
| Units of P available now | stock changes | Inventory.quantity | product page badge (live read, no copy) |
| Units reserved by order O | checkout | OrderItem.quantity | none |
| Whether order O is paid | the payment provider | the provider's charge record | Order.paymentStatus + timestamp of the last webhook; re-checkable by API |
| Whether order O was delivered | the shipping provider | the carrier's tracking record | Order.shippingStatus, polled or pushed, with the time it was last seen |
Where the question leads
The question ladder shows the same confusion asked three ways. The vague form is unanswerable because it assumes one fact; the best form names the reader's question and asks for an owner, which is a question the schema can be made to answer.
why The best form separates the two facts, names the moment the second one is decided, and asks for a distinct name — so the answer is a column and a capture step, not an opinion about which table is more trustworthy. The vague form has no answer because it is asking one question about two things.
How to do it
Most important first.
- For every number the disputed screens show, write the reader's question in full: "the current list price of P" versus "the amount charged to C for P on order O". If two screens ask different questions, they are allowed to show different numbers.
- For each question, name one owner and why: it is decided there. Product price: the product record, because the admin edits it there. Amount paid: the order item, because checkout decided it — with the provider's charge as the external witness.
- Rename until no two facts share a name. Then audit every reader against the table: each should read exactly the owner of the fact it asked about.
- For every copy that survives, write down what it copies and when it was taken, and whether it can be re-derived from the owner if it turns out wrong (Snapshots vs References).
Worked on a concrete problem
The move has to produce something. This is what it produced.
- The three totals. Cart: "the sum of the current prices of the items in the cart" — owner Product, so the cart reads live, and the total may legitimately differ from a total seen yesterday. Confirmation email: "what was this order's total when it was placed" — owner OrderItem.unitPrice × quantity, captured by checkout. Admin order view: the same question as the email, so the same owner. The bug was that the email and the admin view were reading the cart's owner.
- Inventory, the same move: "how many units are available now" is owned by the stock record; "how many did this order reserve" is owned by the order line. Reading the live count in the order view gives a number that changes as other customers buy — correct for the stock page and meaningless on an order.
- Payment status: "has this order been paid" is *decided* by the provider, so the provider is the source of truth and Order.paymentStatus is a copy taken when the webhook arrived. Which means the order can say "paid" while the provider has since reversed the charge; the copy needs a timestamp and a path to re-check (What If Payment Fails?).
How you know it worked
What now exists that did not before, and what question you can now ask.
- The disputed screens have been sorted into groups by the question they ask, and screens in different groups are allowed to disagree by design.
- No two facts share a name. Someone reading the schema can tell a live price from a captured one without reading code.
- For each copy you can say what it copies, when, and how it would be re-derived; for each externally-decided fact you can say who decides it.
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.
- ?What question is this reader actually asking — and do two readers with the same word mean the same fact?
- ?Where is this fact decided, as opposed to where is it convenient to read?
- ?Which of the places holding this value are copies, what are they copies of, and when were they taken?
- ?Which facts in this system are decided outside it, and what does my record of them claim to be?
What can go wrong
- Everything becomes a distinct fact. "The price on the product page" and "the price on the catalog card" are the same question, and splitting them creates the duplication the move exists to remove.
- The owner is chosen for convenience — the place the code already reads — rather than for where the fact is decided. A cache is never the source of truth, however fast it is.
- The move is applied to the schema only. The frontend keeps its own copy of the cart total and computes it differently; source of truth is a property of the whole system, including the browser (Who Owns This State?).
- External sources of truth are ignored because they are inconvenient. Declaring the database authoritative for "paid" does not make it so; the provider still decides, and the day they disagree the provider wins.
- Splitting one field into two facts adds a column, a capture step and a rename, and every future reader has to choose. The cost is real; it is paid once per fact and saved on every subsequent bug.
- Naming the provider as the source of truth for payment means your own database can never fully answer "is this paid?" without a caveat — which is honest, and also slower and more code.
- A source-of-truth table is a document, and documents drift. It stays true only if the schema names carry the distinction, so that the table is readable from the code.
- "Single source of truth means one database." It means one owner per fact; a system can have many facts with many owners, some of them outside. One database holding two versions of the price has no single source of truth for anything.
- "Copies are bad." Copies with a stated origin and time are how history works; the order item *is* a copy of the price, deliberately. The bad copy is the one that does not know it is a copy.
- "The provider is authoritative, so trust the webhook." The provider is authoritative for the charge; the webhook is a message about it that can arrive late, twice or out of order. Authority and the channel that reports it are different things (Duplicate Requests).
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.
- GENERALEvery system with more than one place a fact is held needs an owner per fact. In a chat app "the last message in this conversation" and "the last message this user has read" are the pair; in a dashboard "the metric" and "the metric as of the report".
- CONTESTEDThe strongest opposing view: choosing one physical store as authoritative for everything and deriving all other views from it — an event log, a canonical table — is simpler to reason about than a per-fact ownership table, because there is exactly one place to look and every copy is provably derived. That view is right when the system is small enough that derivation is cheap and no fact is decided outside it; it stops being right the moment a payment provider decides something your log can only record.
- ILLUSTRATIVEThe three disagreeing totals and the reversed charge are invented; the ownership table is one reasonable assignment for a small store, not the only one.
Where the depth lives
This domain asks the question and hands the answer off by name.