In document modelling, when should a reference also carry a copy of a few fields?
answer
- Neither pure embed nor pure reference
- The hot read needs two fields, not twenty
- Keep the identifier so you can re-resolve
- Some copies are snapshots, not caches
- Never copy what a decision depends on
basics
~20 sCopy a field next to its reference when the hot read needs only that field and resolving the whole referenced document would cost an extra round trip. Prefer fields that are immutable or that record a point-in-time value.
solid answer
~50 sThis is the middle ground between the two pure models. Keep the identifier so the full record is always resolvable, and alongside it copy the one or two fields the common read actually renders — a product's name on an order line, an author's display name on a post. The hot path becomes a single read; the rare path that needs everything still resolves the reference. The judgment is about *which* fields. The safest copies are values that never need syncing at all: a point-in-time fact such as the price charged at purchase is not a stale cache, it is the correct value forever. Next safest are slow-changing display fields where staleness for a few minutes is invisible. Never copy something a decision is made on, something with a legal freshness requirement, or a high-churn counter. The mirror image of the technique is keeping a bounded slice of the children — the newest handful — in the parent while the full set lives in its own collection.
code
json · 11 lines{
"_id": "order-771",
"lines": [
{
"productId": "p-19",
"nameAtPurchase": "Cast iron skillet",
"priceAtPurchase": { "amount": 4900, "currency": "EUR" },
"qty": 1
}
]
}go deeper
Know that the choice is not strictly either-or: a document can hold a reference and a small copy of a field or two so the common read does not need a second lookup.
Explain what the copy buys — one read instead of two — and what it costs: a second place the value lives, which something has to keep current or repair.
Classify fields before copying them: snapshot values that must never change, slow-changing display fields with an agreed staleness window, and fields that must always be read fresh because a decision depends on them.
Set the policy on where duplicated fields are allowed, who owns each copy's freshness, and when a screen should be served by a purpose-built read model instead of by copies scattered across the write model.
## Why a middle ground exists Pure embedding and pure referencing each fail in a predictable way. Embedding a shared, mutable entity means editing it everywhere. Referencing it means an extra lookup on every read, including reads that need almost nothing from the target. Most real screens sit exactly in the gap: an order-history page shows fifty line items, needs the product's name and nothing else from the product record, and would otherwise resolve fifty referenced documents to render fifty strings. The hybrid keeps the reference *and* copies the fields the hot read needs. It is deliberately not a full embed — the copy is small, explicitly partial, and the identifier is still there so the authoritative record can be resolved whenever more is required. ## The two directions of the technique **Reference plus a few fields.** The parent stores the child's identifier and a small projection of it. The read path renders from the copy; the write path and any screen needing the full entity follows the identifier. **Reference plus a bounded slice.** The reverse case, for one-to-many. The children live in their own collection because they are unbounded, but the parent keeps the newest few inline so the common page is one read. The slice is a cache of the collection, never the record of truth, and must be trimmed on every append so it stays bounded. ## Choosing what to copy This is the whole skill, and it turns on how the copied value relates to time. **Snapshot values need no syncing at all.** The price charged on an order, the address a parcel was shipped to, the tax rate applied, the terms accepted — these are not stale copies of the current value, they are the historically correct value, and updating them would be a bug. Copying these is unambiguously right, and the model should name them so the intent is legible: `priceAtPurchase`, not `price`. **Immutable identity fields are nearly as safe.** Something that never changes after creation can be copied freely. **Slow-changing display fields are the real trade.** A display name, a product title, a category label. These do change, so the copy can go stale, and the question becomes how long staleness is acceptable and what refreshes it. That is a product conversation as much as a technical one, and it should end in a stated tolerance, not a shrug. **Some fields must never be copied.** Anything a decision is made on — an entitlement, a permission, an account status, a credit limit — must be read from the authoritative record, because a stale copy is not a cosmetic defect but a wrong decision. Anything with a legal or regulatory freshness requirement is in the same class. So are high-churn values such as live counters, where the copy is wrong more often than it is right and the sync traffic swamps the saving. ## The cost you are taking on Every copied field is a second place the value lives. Something has to update it when the source changes, and something has to detect and repair the copies that were missed. That cost is real and it grows with the number of copied fields and the churn rate of each. It is also why the technique should stay narrow: copy two fields, not the whole record. The moment a team starts copying "the fields we might need", the hybrid has quietly become a full embed with none of the deliberation. ## Keeping it honest in a codebase A few habits keep the pattern from rotting. Always keep the identifier, so the copy can be discarded and rebuilt from the source at any time. Name copied fields so their nature is visible in the document — a snapshot named for its moment, a cached label named as denormalised. Write down the tolerated staleness per field, because "eventually" is not a specification. Run a periodic comparison that reports divergence rather than silently rewriting, so you find out whether the sync path actually works. And re-examine the copy when the read path changes: a field copied for a screen that no longer exists is pure liability. ## A worked shape An order line holds `productId`, `nameAtPurchase` and `priceAtPurchase`. The last two need no synchronisation ever — they are what was sold and what was charged. If the catalogue screen later needs the current thumbnail URL on the same line, that is a genuinely different decision: a mutable display field, copied only if the staleness window is agreed and something keeps it fresh, and otherwise resolved through `productId`. ## What an interviewer is listening for That you reach for the hybrid deliberately rather than by accident; that you can classify a field as snapshot, slow-changing or must-be-fresh; and that you name the ongoing maintenance cost instead of presenting the copy as free performance.
- Why is a price stored on an order line not a stale copy of the product's price?Because it records a different fact. The product record holds what the item costs now; the order line holds what the customer was actually charged, which is fixed at the moment of sale. Refreshing it would rewrite history and break every receipt and report. Name it for the moment it captures so nobody mistakes it for a cache and wires it into a sync job.
- When does a copied display name stop being worth it?When the field churns often enough that the sync work or the staleness complaints exceed the saved lookups, or when the copies have spread far enough that no single owner can update them all. At that point resolve the reference on read, or serve the screen from a purpose-built read-optimised view that one job rebuilds.
- How does keeping a bounded slice of children in the parent differ from embedding them?The slice is explicitly not authoritative. The complete set lives in its own collection, the slice holds only the newest few, it is trimmed on every append, and it can be dropped and rebuilt from the source at any time. Embedding makes the array the only copy, which is what removes the ceiling on growth.
saying these in an interview costs you the question
- Copies the whole referenced record and calls it a hybrid
- Treats a snapshot value as something needing synchronisation
- Copies an authorization or status field onto many parents
- Drops the identifier once the fields are copied
- Presents the copy as free with no staleness owner