100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
MongoDB

Embedding vs Referencing

Understand the core MongoDB modeling trade-off between embedding related data inside a document and referencing it via a separate collection.

Schema DesignIntermediate8 min readJul 10, 2026
Analogies

The Core Trade-off

Embedding places related data as a nested sub-document or array directly inside a parent document, so a single find() call retrieves everything needed to render a view. Referencing instead stores an _id (or array of _ids) pointing to documents in another collection, requiring a second query or a $lookup aggregation stage to assemble the full picture. Embedding trades some duplication and update complexity for read performance and atomicity — a single-document write is atomic in MongoDB, whereas coordinating writes across two referenced documents is not, absent a multi-document transaction. Referencing trades a bit of read latency for normalization, smaller documents, and independence between the two entities' lifecycles.

🏏

Cricket analogy: A scorecard that prints each batsman's full career stats inline (embedding) lets you read everything in one glance, versus a scorecard that just lists player IDs you'd have to look up separately (referencing) — the first is faster to read, the second stays lean and reusable across matches.

One-to-Few, One-to-Many, One-to-Squillions

MongoDB modeling literature commonly splits one-to-N relationships into three regimes. One-to-few (a handful of items, like a user's two or three addresses) is almost always embedded as a small array — there's little downside and it's fast to read. One-to-many (dozens to low-hundreds, like a product's reviews) is a judgment call: embed if the array stays bounded and is always read with the parent, reference if reviews are paginated independently or can grow large. One-to-squillions (unbounded, like a sensor's time-series readings or a user's activity log) should almost always be referenced, typically with the parent's _id stored on the child (parent-referencing), since embedding would blow past reasonable document sizes and slow every write to the parent.

🏏

Cricket analogy: A player's shirt number is a one-to-few relationship (one player, a handful of jersey numbers across career), easily embedded, while every ball he's ever faced is one-to-squillions and lives in a separate ball-by-ball database, not on his player card.

Using $lookup When You Do Reference

When data is referenced, the aggregation framework's $lookup stage performs a left outer join, matching a localField on the source collection against a foreignField on the target collection and producing an array of matched documents. This is the standard way to reassemble referenced data for a single query rather than issuing N+1 round trips from application code. $lookup is powerful but not free: it typically requires an index on the foreign field to avoid a full collection scan for every input document, and joining across sharded collections has additional restrictions since MongoDB 5.0's improvements around routing. For hot paths, many teams still prefer denormalizing a few frequently-needed fields (like a product's name and price) onto the referencing document to avoid a $lookup entirely.

🏏

Cricket analogy: A TV broadcast overlay that pulls a bowler's career economy rate live from a separate stats database, matching on the bowler's ID, is exactly what $lookup does — joining two feeds by a shared key at query time rather than pre-printing every stat on the ball-by-ball feed.

javascript
// Referencing: orders store only a customerId
db.orders.insertOne({ _id: ObjectId(), customerId: ObjectId("64f1..."), total: 249.99, items: [/* ... */] });

// Reassembling with $lookup instead of a second app-level query
db.orders.aggregate([
  { $match: { customerId: ObjectId("64f1...") } },
  {
    $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" },
  { $project: { total: 1, "customer.name": 1, "customer.tier": 1 } }
]);

// Requires an index for performance at scale
db.customers.createIndex({ _id: 1 }); // _id is indexed by default, but foreign fields often aren't

Rule of thumb from the MongoDB manual's data modeling guidance: prefer embedding for one-to-few, prefer referencing for one-to-squillions, and treat one-to-many as a judgment call based on whether the child data is bounded and always consumed with the parent.

$lookup without an index on the foreign field forces a collection scan per matched document on the 'from' collection, which can silently turn a fast query into a slow one as data grows — always index the field you're joining on.

  • Embedding co-locates related data for atomic, single-round-trip reads; referencing normalizes data across collections.
  • Single-document writes are atomic in MongoDB; cross-document consistency needs care or explicit transactions.
  • One-to-few relationships are almost always embedded; one-to-squillions relationships are almost always referenced.
  • One-to-many is a judgment call based on boundedness and whether the child is always read with the parent.
  • $lookup performs a left outer join across collections at query time and needs an index on the foreign field.
  • Denormalizing a few hot fields onto the referencing document is a common way to avoid $lookup on critical paths.
  • The right choice depends on the specific application's read/write patterns, not a universal rule.

Practice what you learned

Was this page helpful?

Topics covered

#MongoDB#MongoDBStudyNotes#Database#EmbeddingVsReferencing#Embedding#Referencing#Core#Trade#StudyNotes#SkillVeris