MongoDB from Basics to Production · MongoDB Basics

Data Modelling: Embedding or Referencing

Design documents around access patterns: embed data read together, reference data that grows or is shared, and join with $lookup when needed.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

Two models for orders: line items embedded in the order document (read together, written together), and customers referenced by ID (shared across many orders and updated independently), combined with $lookup for an order-history page.

The idea

  • Model for how the application reads and writes data, not for normal forms.
  • Embed when data belongs to one parent, is read with it, and is bounded (order lines, an address).
  • Reference when data is shared, large, updated independently, or grows without limit (customers, comments on a popular post).
  • $lookup joins collections in an aggregation; useful, but if you need it on every hot read, reconsider the model.
  • A document can be at most 16 MB; unbounded arrays are a design bug.

The code

Embedded order lines, referenced customers
db.customers.insertMany([
  { _id: "c1", name: "Ana", tier: "gold" },
  { _id: "c2", name: "Ben", tier: "standard" },
]);
db.orders.insertMany([
  { _id: "o1", customerId: "c1", status: "paid",
    lines: [{ sku: "mug", qty: 2, price: 8.5 }, { sku: "tea", qty: 1, price: 12 }] },
  { _id: "o2", customerId: "c2", status: "paid", lines: [{ sku: "teapot", qty: 1, price: 24 }] },
  { _id: "o3", customerId: "c1", status: "open", lines: [{ sku: "kettle", qty: 1, price: 31.9 }] },
]);

// One read returns the order with all its lines (embedded)
const o1 = db.orders.findOne({ _id: "o1" });
print(o1._id, o1.lines.map(l => `${l.qty}x ${l.sku}`).join(", "));

// Order history with customer names (referenced, joined on demand)
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } },
  { $unwind: "$customer" },
  { $project: { _id: 1, customer: "$customer.name",
                total: { $sum: { $map: { input: "$lines", in: { $multiply: ["$$this.qty", "$$this.price"] } } } } } },
  { $sort: { _id: 1 } },
]).forEach(d => print(d._id, d.customer, d.total));

Output

o1 2x mug, 1x tea
o1 Ana 29
o2 Ben 24

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Designing like a relational schema (one collection per table, $lookup everywhere).
  • Unbounded embedded arrays (all comments, all log events) that grow towards the 16 MB limit and slow every update.
  • Duplicating data that changes often (customer email in every order) without a plan to keep copies in sync; duplicate only what's stable or historical (the price at the time of order).

Key points

  • Model around access patterns.
  • Embed bounded data read together; reference shared or unbounded data.
  • Use $lookup for occasional joins, not every hot path.

Further reading: Data modeling · $lookup

Practice questions

When should you embed related data?

Answer

When it belongs to one parent, is read together with it, and is bounded in size, such as order lines.

Why are unbounded arrays a problem?

Answer

Documents have a 16 MB limit and growing arrays make every read and update of the document slower; store such items in their own collection.

What's a sensible use of data duplication in MongoDB?

Answer

Copying values that are historical or rarely change, such as the product price at the time of an order, into the referencing document.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?