Embedding vs referencing
Design for how the application reads and writes. Learn the rules of thumb for embedding versus referencing based on relationship cardinality, growth and access patterns.
- Model one-to-one, one-to-few, one-to-many and many-to-many relationships
- Decide between embedding and referencing
- Recognize unbounded growth and the 16 MB risk
- Accept and manage controlled duplication
conceptThe concept
Open the interactive lesson to use this diagram.
The guiding rule: data that is accessed together should be stored together. Start from the queries, not the entities.
| Relationship | Typical design | Example |
|---|---|---|
| One-to-one | Embed | customer and its preferences |
| One-to-few (bounded) | Embed an array | customer addresses, order line items |
| One-to-many (hundreds+) | Reference from the "many" side | orders.customerId |
| One-to-squillions | Reference from the child, never an array in the parent | events per user |
| Many-to-many | Array of ids on one or both sides, or a link collection | students and courses |
Embed when the child is read with the parent, is bounded in number, and is owned by the parent. Reference when the child is large, unbounded, shared by many parents, updated independently, or queried on its own.
whyWhy it matters
Schema is the biggest lever on performance. A good schema makes the common query a single indexed read; a bad one needs joins, multiple round trips, or ever-growing documents that degrade every write. Fixing a schema later means a data migration on a live system.
Use caseReal-world scenario
The shop embeds line items in orders and copies name, category and price from the product at purchase time. That copy is intentional: the order must show what the customer paid even if the product price changes tomorrow. Reviews live in their own collection because a product can have thousands.
Try itExamples
Run and edit these in the interactive lesson.
db.orders.findOne({ orderNo: "ORD-100123" }, { _id: 0, orderNo: 1, status: 1, items: 1, shipping: 1, payment: 1 })db.reviews.countDocuments({ productId: "P010" })
db.reviews.find({ productId: "P010" }, { _id: 0, rating: 1, title: 1, helpfulVotes: 1 }).sort({ helpfulVotes: -1 }).limit(3)const line = db.orders.findOne({ "items.productId": "P003" }, { items: { $elemMatch: { productId: "P003" } } }).items[0]
const product = db.products.findOne({ _id: "P003" }, { price: 1 })
print("paid at purchase:", line.price, " current catalog price:", product.price)db.orders.aggregate([
{ $project: { size: { $bsonSize: "$$ROOT" }, lines: { $size: "$items" } } },
{ $group: { _id: null, avgBytes: { $avg: "$size" }, maxBytes: { $max: "$size" }, maxLines: { $max: "$lines" } } }
])internalsUnder the hood
Bigger documents mean fewer documents per page in the WiredTiger cache, so if most reads only need a few fields of a large document, you waste cache and IO. Every update rewrites the whole document in the storage engine. These are the physical reasons behind "bounded embedding" and the subset pattern.
Avoid / preferDo this, not that
{ _id: 66, name: "Asha", orders: [ /* every order she ever placed */ ] }// customers: { _id: 66, name: "Asha" }
// orders: { customerId: 66, createdAt: ..., items: [...] }
db.orders.createIndex({ customerId: 1, createdAt: -1 })Orders grow without bound and are queried on their own. A separate collection with an index on customerId scales; an embedded array eventually hits 16 MB.
// addresses: { customerId: 66, city: "Pune", zip: "411001" }
db.customers.aggregate([{ $lookup: { from: "addresses", ... } }])db.customers.insertOne({ _id: 66, name: "Asha",
address: { city: "Pune", zip: "411001" } })A one-to-one child that is always read with its parent belongs inside it. A separate collection only adds a join.
PitfallsCommon mistakes
- Copying a relational schema one table per collection and joining everything with
$lookup. - Embedding unbounded children (all orders inside the customer).
- Refusing any duplication. Duplicating fields that rarely change (names, prices at a point in time) is normal; just decide how each copy is updated.
- Designing without listing the top queries and their frequency first.
LimitsWhen not to use it
Embedding is wrong when the child is shared by many parents and changes often, when the child list is unbounded, or when children are queried on their own at a high rate. Referencing is wrong when the data is always read together and small. Decide from the access patterns, not from entity diagrams.
SecuritySecurity notes
Embedding mixes sensitivity levels. If a customer document embeds payment tokens or identity documents, every query that returns the customer risks returning them. Keep sensitive data in a separate collection with tighter roles, or encrypt the fields client-side.
ChallengeTry it yourself
To decide whether embedding line items is safe, compute the average and maximum number of items per order.
Show one solution
db.orders.aggregate([
{ $group: { _id: null, avgItems: { $avg: { $size: "$items" } }, maxItems: { $max: { $size: "$items" } } } },
{ $project: { _id: 0, avgItems: { $round: ["$avgItems", 2] }, maxItems: 1 } }
])InterviewQuestions you should be able to answer
How do you decide between embedding and referencing?
Ask: Is the child read together with the parent? Is the number of children bounded and small? Is the child owned by one parent? Is it updated independently or at high frequency? Embed if the answers favour locality and boundedness; reference if the child is unbounded, shared, large, or accessed alone. Then validate with the top queries, their frequency, and the write patterns.
How do you model many-to-many?
Usually an array of ids on the side that is read most (for example course.studentIds if bounded, or student.courseIds). If both sides are large or the relation has attributes (enrollment date, grade), use a link collection enrollments { studentId, courseId, ... } indexed on both fields.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.