iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Embed vs Reference

The single biggest schema decision in MongoDB is whether to embed a related document inline or to reference it by id. Embedding wins for "always read together" data with a 1:few relationship; referencing wins for "many writers", "huge sub-arrays", or many-to-many. Get it wrong early and every read or write pays the bill.

A decision matrix with worked examples

EXAMPLE
// ===== When to EMBED =====
// - Sub-document is read with the parent almost every time
// - The lifecycle is the same (deleting the parent deletes the children)
// - The sub-array stays bounded (< a few hundred items, < 16MB doc cap)
// - Updates touch the WHOLE document together
// Examples: order with line items, post with comments (capped), user profile prefs

const order = {
  _id: 'o1',
  customer: 'alice',
  items: [
    { sku: 'sku-1', qty: 2, price_cents: 1995, name: 'Wool jacket' },
    { sku: 'sku-2', qty: 1, price_cents: 8900, name: 'Linen shirt' },
  ],
  total_cents: 12790,
  status: 'new',
  created_at: new Date(),
};

// Get the whole order in ONE round trip — no $lookup, no fan-out
const o = await db.orders.findOne({ _id: 'o1' });

// Push a new line item atomically (still one round trip)
await db.orders.updateOne(
  { _id: 'o1', status: 'new' },
  { $push: { items: { sku: 'sku-3', qty: 1, price_cents: 5500, name: 'Beanie' } },
    $inc:  { total_cents: 5500 } }
);

// ===== When to REFERENCE =====
// - Sub-document is read INDEPENDENTLY from the parent
// - The sub-array grows unbounded (timeline of activity, audit log)
// - Many parents share the same child (categories, tags, authors)
// - The child has its own concurrent writers
// Examples: posts -> comments (unbounded), tickets -> events, users -> orders

// Customers and orders — separate collections with an FK-style field
await db.customers.insertOne({ _id: 'c1', name: 'Alice', email: 'alice@example.com' });
await db.orders.insertMany([
  { _id: 'o1', customer_id: 'c1', total_cents: 12790, created_at: new Date('2026-06-15') },
  { _id: 'o2', customer_id: 'c1', total_cents:  9900, created_at: new Date('2026-06-17') },
]);

// Read both with $lookup (one round trip, but cross-collection)
const withOrders = await db.customers.aggregate([
  { $match: { _id: 'c1' } },
  { $lookup: { from: 'orders', localField: '_id', foreignField: 'customer_id', as: 'orders' } },
]).next();

// ===== HYBRID: embed a denormalised SUMMARY, reference the full record =====
// Best of both for product cards / order rows
const post = {
  _id: 'p1',
  title: 'Welcome',
  body: '...',
  author_id: 'u1',
  author_summary: { name: 'Alice', avatar_url: '/u/alice.jpg' },    // snapshot
  comment_count: 124,
};
// Reads are fast (no lookup for the card). Authors collection is the source of truth.
// Write the summary on author updates via a small background job or a change-stream.

// ===== Anti-patterns =====
// 1) Unbounded arrays in embedded docs
//    posts.comments: [....] -> works for 50 comments, breaks at 50,000 (16MB cap, slow updates).
//    Fix: reference + paginated query.
// 2) Two-way denormalisation that gets out of sync
//    author.name copied into every post; rename the author -> 1M updates.
//    Use sparingly, and rebuild on a job.
// 3) Joining everything with $lookup as if Mongo were a SQL database
//    Few $lookups are fine; many means the schema is wrong.

// ===== Sizing rules of thumb =====
//   Embed if: 'always read together' AND 'parent owns the children' AND 'bounded growth'
//   Reference if: 'shared / unbounded / concurrent writers'
//   Hybrid if: 'list pages need a quick summary but you have a single source of truth'

Why it matters

When in doubt, embed first and split later. Reads dominate most workloads and one-document reads win on round trips, atomicity, and cache locality. The migration to a referenced shape later is a one-shot script; the migration FROM unbounded embedded arrays to references is a multi-week incident.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Embed: comments inside a post (reads = one round-trip).
// Reference: customer's orders (avoid huge growing arrays).
Try it Yourself »

Discussion

Loading…