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.
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).
$lookupjoins 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
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,
$lookupeverywhere). - 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.