Embedding vs Referencing
This is the single most important decision in MongoDB schema design. Unlike relational databases, where normalization is close to the default, MongoDB gives you a real choice on every relationship: embed the related data inside the parent document, or reference it by storing an ID and looking it up separately. Get this wrong at scale and you either bloat documents past usefulness or pay for a $lookup on every read.
The Core Trade-off
Embedding | Referencing | |
|---|---|---|
Reads | One query, no joins | Needs $lookup or extra round trip |
Writes | Whole document rewritten | Independent, smaller writes |
Document size | Grows with related data | Stays small and stable |
Data duplication | Higher (unless extended reference) | None |
Atomicity | Single-document (always atomic) | Needs a transaction across docs |
Best for | Data always read together | Independently updated / huge / shared data |
The One-to-Few / One-to-Many / One-to-Squillions Framework
A widely used mental model from MongoDB's own architects splits every relationship into three buckets based on cardinality — how many related items there can be.
One-to-few (a handful of items, e.g. a person's 2-3 addresses): embed as an array. They rarely grow, and are always read with the parent.
One-to-many (dozens to thousands, e.g. an order's line items, a blog post's comments): usually embed, but consider referencing if the child list grows large or is queried on its own.
One-to-squillions (unbounded, e.g. a sensor's millions of readings, an app's log events): always reference — often with the parent reference pattern (store the parent ID on each child, not a giant array on the parent).
Rule of Thumb: Read/Write Patterns Decide
Cardinality is a starting point, not the whole answer. The real question is: how is this data accessed?
Data that is always displayed together and rarely changes independently → embed.
Data that is queried, sorted, or paginated on its own, independent of the parent → reference.
Data that is updated far more often than the parent (or by a different part of the system) → reference, so you avoid rewriting the whole parent document on every small change.
Data shared across many parents (e.g. a "product" referenced by thousands of orders) → reference, to avoid duplicating and having to update N copies.
Worked Example: Blog Post and Comments
Say we're modeling a blog. A post has an author, tags, and comments. Let's walk through the same domain both ways.
Embedding — good for small, bounded, always-together data
// Tags: one-to-few, always read with the post → embed
{
_id: ObjectId("..."),
title: "Understanding Embedding vs Referencing",
tags: ["mongodb", "schema-design"], // embedded array
author: { // embedded sub-document
name: "Priya Shah",
avatarUrl: "https://cdn.example.com/priya.png"
}
}
// One query returns everything needed to render the post header.Referencing — good for unbounded, independently-queried data
// Comments: one-to-squillions, queried/paginated independently → reference
// posts collection
{ _id: ObjectId("post1"), title: "Understanding Embedding vs Referencing" }
// comments collection — each comment references its post
{ _id: ObjectId("c1"), postId: ObjectId("post1"), author: "Alex", text: "Great write-up!" }
{ _id: ObjectId("c2"), postId: ObjectId("post1"), author: "Sam", text: "Clear examples." }
db.comments.createIndex({ postId: 1, createdAt: -1 })
db.comments.find({ postId: ObjectId("post1") }).sort({ createdAt: -1 }).limit(20)
// A post with 50,000 comments never blows up — you paginate the child
// collection instead of loading a giant embedded array.The Document Growth Problem
Embedding an unbounded array causes real operational problems as the document grows:
The 16 MB BSON document size limit becomes a real ceiling for very active parents (e.g. a product with a lifetime of reviews).
WiredTiger must rewrite the whole document on every array push once it no longer fits in its previously allocated space, causing extra I/O and fragmentation.
Every read of the parent pulls the entire array over the wire even if you only need the count or the last three items.
Indexes on deeply nested/large arrays get expensive to maintain (multikey index entries scale with array length).
Extended Reference — the Middle Ground
A common hybrid: reference the related document by ID, but also duplicate the two or three fields you display everywhere (name, thumbnail) directly in the parent. You avoid the join for the common case, and only reference for the full detail view.
// Order embeds just enough product info to render the order list
// without a $lookup, but keeps productId to fetch full details when needed
{
_id: ObjectId("order1"),
items: [
{ productId: ObjectId("p1"), name: "Wireless Mouse", price: 24.99, qty: 2 }
]
}Decision Checklist
Is the related data always read together with the parent? → lean embed.
Could the related data grow without bound? → lean reference (or bucket/cap).
Is the related data updated independently and frequently? → lean reference.
Is the related data shared by many parents? → lean reference (avoid update fan-out).
Do I need atomic multi-field updates across parent and child in one operation? → embed (single-document atomicity), or use a transaction if you must reference.
Is the read latency of a $lookup acceptable for this access pattern? → if yes, referencing is fine even at moderate cardinality.