MongoDBEmbedding vs Referencing

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

JS
// 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

JS
// 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).

Warning
If an embedded array has no natural upper bound (comments, log events, order history across a customer's lifetime), do not embed it directly on the parent. Either cap it (keep only the last N and reference the rest), bucket it (see the Bucket Pattern), or move it to its own referenced collection.
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.

JS
// 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 }
  ]
}
Tip
Extended reference trades a little duplication (and the need to update copies when the source changes, usually via a background job or change stream) for avoiding a join on your hottest read path.
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.

Note
There is no universal right answer — the same "comments" relationship might be embedded for a low-traffic internal tool and referenced for a public blog with millions of comments. Model for your actual access pattern, not the relationship's name.