advanced~3h

MongoDB Schema Design — Embedding vs. Referencing

There's no ALTER TABLE debate in MongoDB — there's exactly one recurring design decision, made once per relationship, that determines whether your app stays fast and simple or turns slow and tangled. This chapter is that one decision, made rigorously.

Learning objectives

  • Apply a concrete decision framework to choose embedding vs. referencing for a given relationship.
  • Explain why the 16MB document limit is a design signal to avoid approaching, not a budget to design toward.
  • Match one-to-few, one-to-many, and one-to-squillions relationships to their correct modeling pattern.
  • Diagnose, from symptoms alone, whether a schema has over-embedded or over-referenced.

There's no ALTER TABLE debate in MongoDB, but there is one recurring design decision, made once per relationship in your data: embed the related data as a nested document/array inside the parent, or reference it by storing just an ID and querying the other collection separately (optionally joined back with $lookup, Chapter 01 §5).

AskLean toward embeddingLean toward referencing
Do they change together?Yes — one update touches bothNo — updated independently, on different schedules
Are they read together, almost every time?Yes — the parent is rarely useful without the childNo — often fetched separately, or not needed at all
Is the child's cardinality bounded?Yes — a handful, or a known small ceilingNo — unbounded or "could grow forever"
Is the child shared across many parents?No — it belongs to exactly one parentYes — the same child data is referenced by many parents

A user profile and its 2-3 saved addresses: change together often enough, read together almost always, bounded count — embed. A product and its reviews, potentially tens of thousands on a popular item: unbounded, and a review's own lifecycle (edited, reported, moderated) is independent of the product's — reference. This single question, asked honestly per relationship, resolves the large majority of MongoDB schema decisions; the rest of this chapter is refinement on top of it.

◆ The problem

A product document embeds its reviews as an array field. With 20 reviews this works perfectly and every product page load fetches everything it needs in one query. On a product that goes viral and accumulates 50,000 reviews, two things go wrong — and the hard 16MB-per-document limit is actually the smaller one.

The practical problem arrives long before the hard ceiling: every read of the product — even a page that only wants to display the product name and price — pulls the entire reviews array along with it, because the whole document is one BSON unit. Every write that appends one new review re-sends and re-serializes the whole document, not just the new element. The document becoming slow to load and expensive to write happens well before it's anywhere near actually full — the 16MB limit is best treated as a hard backstop that should never realistically be approached, not a budget to design toward.

▲ Common mistake

Treating "does it technically fit under 16MB" as the design question. The real question is whether the child collection can grow without a known bound — reviews, comments, event logs, sensor readings. If a sub-array's growth has no natural ceiling, embedding it is usually the wrong call regardless of current size, because the document only gets slower to touch as it grows, for every operation, not just the ones that need the array.

The fix for a child collection that's unbounded but still wants "recent N, fast" access is usually the subset pattern: embed only the most recent or most relevant N items (say, the 10 most helpful reviews) directly in the parent for fast common-case reads, and keep the full, unbounded history in its own referenced collection for anything that needs to go further.

Relationship shapeExampleApproach
One-to-fewA user and their 2-3 shipping addressesEmbed directly as an array field
One-to-manyA product and its hundreds of reviewsReference — own collection, parent holds no array at all, or only a bounded "recent/top N" subset (§2's subset pattern)
One-to-squillionsA sensor and millions of readings, a user and years of activity-log eventsAlways reference, and reverse the reference — the child stores the parent's ID, the parent stores nothing about its children at all

The one-to-squillions case is worth calling out specifically because the instinct to embed a reference (an array of reading IDs inside the sensor document) is still wrong at that scale — an array of a million IDs has the exact same unbounded-growth problem as embedding the readings themselves, just with smaller elements. The correct shape has the relationship stored in exactly one place: each reading document carries a sensorId field, and you query readings.find({ sensorId }) to go from parent to children — the parent document never grows no matter how many readings accumulate.

// reading document — carries the reference, not the other way around { "_id": ..., "sensorId": "sensor-042", "value": 21.4, "recordedAt": ISODate("...") } // finding a sensor's readings is a query on the CHILD collection, not an array lookup db.readings.find({ sensorId: "sensor-042" }).sort({ recordedAt: -1 }).limit(100);

💻 Code example

// reading document — carries the reference, not the other way around { "_id": ..., "sensorId": "sensor-042", "value": 21.4, "recordedAt": ISODate("...") } // finding a sensor's readings is a query on the CHILD collection, not an array lookup db.readings.find({ sensorId: "sensor-042" }).sort({ recordedAt: -1 }).limit(100);

Embedding is a form of denormalization — the same data (or a snapshot of it) can end up copied into multiple documents. That's a deliberate, often correct trade in MongoDB, but it has to be made on purpose, per field, not by default.

FieldDenormalize it?Why
Item price, at the moment an order was placedYes — embed a snapshotAn order's total must stay historically accurate even after the product's live price changes later; this isn't staleness, it's the correct value for that order forever
Product's live price, shown on a catalog pageNo — reference and read liveA catalog page showing last month's price after a markdown would be a real bug, not a feature

▲ Common mistake

Embedding a frequently-changing field — a user's display name copied into every comment they've ever posted, for fast comment-list rendering — creates an update-everywhere problem: a single name change now requires a multi-document update across every comment that user ever made, the exact write-fan-out this chapter's cardinality patterns are meant to avoid. The general fix is the extended reference pattern: embed only the small, genuinely-needed subset of fields the read path actually displays (e.g. just displayName and avatarUrl, not the user's whole profile), accept that this subset can occasionally go stale, and have an explicit, deliberate process (a background job, or an event-driven update) for propagating the rare change — rather than treating "update everywhere" as a routine, synchronous part of every name-change request.

Both directions fail, and they fail with different, recognizable symptoms.

SymptomLikely mistakeFix
Individual documents are large; simple field updates feel slow; document size creeping toward the 16MB ceilingOver-embedding — an unbounded child was embeddedMove the unbounded part to its own referenced collection (§3's one-to-many/squillions pattern)
Even simple page loads require several $lookup joins or multiple sequential round trips to assemble one screenOver-referencing — data that's always read together was split apart anywayEmbed the genuinely-bounded, always-together parts (§1's decision table)
A single field change (e.g. a display name) requires updating many documents across a collectionDenormalized a field that changes oftenExtended reference pattern (§4) — embed less, or accept controlled staleness

The over-referencing failure is the one relational-background engineers hit most often, because normalizing everything and joining at read time is the reflexive first move coming from SQL — and it works, but it quietly throws away the exact advantage (fetching one whole, self-contained entity in one query) that made a document database worth choosing for that data in the first place.

Want a visual for this concept?

Generate a diagram tailored to “MongoDB Schema Design — Embedding vs. Referencing” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to MongoDB Performance — Explain Plans, Covered Queries & Pagination← Back to all MongoDB chapters