skip to content

Denormalization & Duplication Trade-offs

Copying the same fact into many documents to make reads fast, and owning the synchronisation debt that creates. The question is rarely 'is duplication allowed?' but 'who updates the copies, and how stale may they get?'

on this pageshow

questions

6

A product's name is copied into every order document. What breaks when that product is renamed?

level: juniorimportance: must knowfreq 68%

answer

  1. the database is not watching the copy
  2. nothing errors when they disagree
  3. ask who owns the update
  4. sometimes the right answer is 'never update it'
  5. name the field for its intent

basics

~20 s

Nothing updates the copies automatically: old order documents keep the old name until some process rewrites them. Either the write path owns propagation, or the copy is deliberately frozen as a historical snapshot and named to say so.

solid answer

~50 s

A document store treats the copied name as ordinary data — there is no constraint linking it back to the product record, so renaming the product changes exactly one document and leaves every order untouched. Nothing errors; screens simply disagree, which is why the bug usually surfaces weeks later. There are only three legitimate owners of that copy: the write path updates source and copies together, an asynchronous propagator rewrites the copies within a bounded lag, or nobody updates it because the copy is intentionally a point-in-time snapshot. The third is often the right answer for an order, and then the field should be called something like `productNameAtPurchase` so no future reader mistakes it for a stale cache. Whichever you choose, you must be able to *find* the copies — that usually means indexing the source id inside the order document.

code

json · 15 lines
json
// Copy with sync debt: must be rewritten whenever the product is renamed
{
  "_id": "order-9001",
  "productId": "prod-42",
  "productName": "Blue Widget",
  "qty": 2
}

// Frozen snapshot: correct forever, nobody should backfill it
{
  "_id": "order-9001",
  "productId": "prod-42",
  "productNameAtPurchase": "Blue Widget",
  "qty": 2
}

go deeper

for a junior

Be ready to say plainly that copied fields are not updated by the database, and that the order documents keep the old name until code rewrites them. Knowing that nothing errors is half the answer.

for a middle

Explain the three possible owners of a duplicated field — write path, background propagator, or deliberately nobody — and why the order document also needs the product id, indexed, before any propagation is possible.

for a senior

Show the operational side: an idempotent resumable backfill, a stored source version so drift is detectable, and a sweep that reports divergence. Argue for freezing the copy where the business fact is genuinely point-in-time.

for a principal

Own the rule for the codebase: every duplicated field is registered with an owner and a maximum staleness, or it is not allowed. Talk about how you keep that rule enforceable as more services learn to write these documents.

## What a copied field actually is When an order document stores `productName: "Blue Widget"`, that string is a copy of a fact whose home is the product document. The database has no idea the two are related. There is no foreign key, no cascade, no trigger implied by the model. The copy is inert bytes that happened to be correct at the moment they were written. Everything about keeping it correct afterwards is an application decision, and if nobody makes that decision explicitly, the default is "the copy is never updated". ## What actually happens on a rename Rename the product and exactly one document changes. Every order document still holds the old string. No read fails, no write is rejected, no log line appears. The order list rendered from order documents alone shows the old name; an admin report that reads the product document shows the new one; a search index rebuilt from orders shows something else again. The system is now internally inconsistent in a way that is invisible until a human notices two screens disagreeing. That silence is what makes duplication debt dangerous compared with a missing field or a type error, which fail loudly on the first read. ## The three possible owners of a copy Every duplicated field must have exactly one answer to "who updates this?": 1. **The write path.** The operation that renames the product also rewrites the copies. This is cheap only when the copies are few and easy to locate, and it makes the rename latency proportional to the number of documents holding the copy. 2. **An asynchronous propagator.** The rename is recorded, and a background consumer or scheduled job rewrites the affected documents in batches. Copies become correct *eventually*, and the design owes you a bound on how long that takes plus monitoring of the actual lag. 3. **Nobody, deliberately.** The copy is a historical snapshot: the order should show the product as it was named when the customer bought it. Then a rename *must not* propagate, and the correct fix is naming — `productNameAtPurchase`, `nameAtOrderTime` — so a future maintainer does not "fix" it by writing a backfill. The weak interview answer is to assume the database handles it. The second-weakest is to pick option 3 without saying so, which is indistinguishable from a bug. ## You must be able to find the copies Propagation only works if you can locate the documents that hold the copy. That means the order document should also carry the product's identifier, and that identifier should be indexed, so "all orders referencing product X" is a targeted query rather than a full collection scan. Storing only the denormalized name and not the id is a common and painful mistake: the copies become unfindable except by matching on the stale string itself, which is exactly the value you are trying to change. ## Partial failure and repair Rewriting many documents is not one atomic act. A propagation job that dies halfway leaves some orders with the new name and some with the old, so the job must be safely re-runnable: filter on documents that still hold the old value, or on a stored source version, and process in resumable batches. Carrying the source record's version or last-updated timestamp inside the copy is what makes divergence *detectable* — a periodic sweep can compare the copy's recorded source version against the product's current one and report or repair the mismatches. Without such a marker, the only audit available is re-reading every source document, which is why teams often discover drift only through customer complaints. ## Choosing not to duplicate The copy exists to avoid a second read. If the product name is edited often, is shown in only a few places, and those places already load other product data, dropping the copy and reading the product document is frequently the better trade. Duplication buys read locality and costs write-time work plus a permanent correctness obligation; you should be able to say what the read actually saved before you accept that obligation. ## How to answer this in an interview Say what breaks (silent divergence, not an error), name the three possible owners, pick one for the scenario and justify it, and mention the two operational requirements: a way to find the copies, and a way to detect divergence. Mentioning that an order's product name is often *correctly* frozen turns a rote answer into a modelling answer.

  • How would you find every order document that holds the stale name?
    Query on the product's identifier, not the name — which means the order document must also store `productId` and that field must be indexed. If you stored only the denormalized string, the copies are findable only by matching the stale value itself, and any order that already drifted for another reason is missed. This is why an embedded copy should always travel with the id of the record it came from.
  • How do you make a propagation job safe to re-run after it crashes halfway?
    Make it idempotent and resumable: select only documents that still carry the old value or an older source version, process in bounded batches, and record progress so a restart continues rather than starting over. Because the rewrite spans many documents it is never atomic, so partial completion is the normal state, not the exception, and the job must converge when run repeatedly.
  • When is it correct for the copy to stay stale forever?
    When the copy is a point-in-time fact rather than a cache of a mutable one. An order should show the product name, price and tax rate as they were at purchase, because that is what the customer agreed to and what an invoice must reproduce. Signal the intent in the field name so nobody backfills it, and keep the reference id alongside for lookups of the current record.

saying these in an interview costs you the question

  • Assumes the database cascades the change to embedded copies
  • Stores the copied name without the source id
  • Treats every stale copy as a bug rather than sometimes a snapshot
  • Writes a one-shot fix script with no re-run safety
  • Says duplication is simply wrong instead of asking who maintains it

context

open as a page

In document data modeling, what is fan-out on write versus fan-out on read, and how do you choose?

level: middleimportance: must knowfreq 62%

basics

~20 s

Fan-out on write copies a fact into every document that will need it at write time, so reads are single lookups. Fan-out on read stores it once and gathers it from many sources per read. Choose by read-to-write ratio and fan-out size.

open as a page

Why is the price paid stored on an order document not the same kind of duplication as a copied product name?

level: middleimportance: should knowfreq 48%

basics

~20 s

The price paid is a distinct historical fact the order owns, so it must never track the product's current price. A copied product name is a cache of a mutable fact elsewhere and carries a synchronisation obligation. Only the second is duplication debt.

open as a page

How do you keep a precomputed count stored inside a document from drifting away from the rows it summarises?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Increment it atomically within its own document, make the increment idempotent so retries cannot double-count, store a marker of what has been counted, and run a periodic recompute from the source that detects and repairs drift instead of assuming none.

open as a page

How do you decide how stale a duplicated field in your documents is allowed to be, and how do you enforce that bound?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Classify each duplicated field by what a wrong read costs: display-only fields tolerate seconds or minutes, while money, permissions and inventory tolerate nothing and must be read from the source. Then pick a propagation mechanism whose measured lag meets that bound, and add a sweep that caps the worst case.

open as a page

A field is duplicated into millions of documents across six collections and must now become editable. How do you decide whether to keep duplicating it?

level: principalimportance: should knowfreq 36%

basics

~20 s

Measure what the duplication actually buys on the read path, then count the maintenance surface: how many writers can create these documents, how often the field changes, and whether the copies are even findable. Keep duplication only where the read win survives an honest edit and repair cost.

open as a page