Document Databases: Embed or Reference
A document store trades joins for locality — the whole object arrives in one read — and the design decision that replaces normalization is whether each relationship is embedded in the parent or referenced by id.
Documents and collections
A document is a JSON-like object: fields, nested objects, arrays. A collection holds documents that need not share a shape. Indexes are declared on fields, including fields inside nested objects and elements inside arrays (multikey indexes). Queries filter by field with operators; the aggregation pipeline chains stages ($match, $group, $lookup, $unwind) for anything beyond a filter.
"Schemaless" means the database does not enforce a shape; it does not mean there is no schema. There is — it lives in the application code and in the assumptions of every query, and it is harder to see and harder to migrate than a CREATE TABLE. Schema validation exists in MongoDB and you should use it.
Embed or reference
Embed when the child is read with the parent every time, is bounded in number, is written with the parent, and should be a snapshot of the moment: an order’s line items and shipping address, a post’s tags. The whole thing is one document, one read, atomically updated. Reference when the child is unbounded (comments on a post), is queried on its own, is shared by many parents (the author of many posts), or is mutable in a way that must be visible everywhere (a user’s display name). Store the id, and do the second lookup — or a $lookup — in the application.
The trap on each side: embedding an unbounded array grows the document until it hits the 16 MB limit and makes every read carry the whole history; referencing everything reproduces a relational schema without the joins, transactions or constraints that made it work.
1// embedded: one read, one atomic write, snapshot semantics2{ _id: "ord_1001", customer_id: "cus_7",3 items: [ { sku: "KB-01", name: "Keyboard", qty: 1, unit_price: 89.00 } ],4 shipping: { line1: "Kastanienallee 12", city: "Berlin" } }5 6// referenced: normalised, joined by the application7{ _id: "ord_1001", customer_id: "cus_7" }8{ _id: "li_1", order_id: "ord_1001", sku: "KB-01", qty: 1 }What you give up, and what you get
Given up: joins (weak, and $lookup is a nested loop), multi-document transactions (supported now, slower and with caveats), foreign keys (none — orphans are your problem), and the planner’s ability to answer a question you did not design for. Received: locality (the object you need is one read), horizontal scaling by sharding on a document key, flexible shape for genuinely variable data, and a data model that matches the application’s objects with no mapping layer.
A useful test: if you catch yourself designing a collection per entity with ids pointing everywhere and $lookup in every query, the workload is relational and a document store is fighting you. If the natural unit of work is one aggregate — an order, a session, a device’s config — the document store is at home.
Key points
- Documents give locality; the design question is embed vs reference per relationship.
- Embed bounded, read-together, snapshot-like children. Reference unbounded, shared, independently-queried or mutable ones.
- Schemaless means the schema is in your code. Enable validation.
- If every query needs $lookup, you wanted a relational database.
Embed or reference?
An order has one shipping address. Embed it in the order document, or reference an addresses collection?
When to use — and when not
- The unit of work is a whole aggregate with variable shape.
- Horizontal scaling on a natural document key with few cross-document queries.
- Many-to-many relationships queried from both sides.
- Invariants across documents.
- Reporting across the whole dataset.
Failure modes
- Unbounded embedded arrays.
- Referenced everything, joins in application code, orphans everywhere.
- No schema validation, five shapes of the same document.
See how this works internally →
Descend one layer: the same topic explained from the machinery up.