1. Embedding vs. Referencing: The Core Tradeoff
Every relationship between two pieces of data in a MongoDB schema resolves to
one of two choices. Embedding nests related data directly
inside the parent document. Referencing keeps it in a separate
collection and stores just an _id to link the two — the same idea
as a foreign key, but MongoDB never enforces or auto-joins it for you.
{
"_id": ObjectId("..."),
"title": "Getting Started with Aggregation",
"author": "Priya Nair",
"comments": [
{ "user": "dev_ana", "text": "This finally clicked for me!", "postedAt": ISODate("2026-01-04") },
{ "user": "kmiller", "text": "Great walkthrough.", "postedAt": ISODate("2026-01-05") }
]
}
// posts collection
{ "_id": ObjectId("post1"), "title": "Getting Started with Aggregation", "author": "Priya Nair" }
// comments collection
{ "_id": ObjectId("c1"), "postId": ObjectId("post1"), "user": "dev_ana", "text": "This finally clicked for me!" }
{ "_id": ObjectId("c2"), "postId": ObjectId("post1"), "user": "kmiller", "text": "Great walkthrough." }
Embedding wins on read performance — one query returns everything, no join required — and keeps genuinely related data physically together. Referencing wins when the related data is large, grows without bound, needs to be queried or updated independently of its "parent," or is shared across many parents at once. The question to ask for every relationship in your schema: "Do I usually need this data together, or separately?"
Relational design starts from the data's structure and normalizes to avoid duplication. MongoDB schema design starts from how your application will query the data — the "correct" shape is whichever one makes your most frequent queries fast and simple, even if that means some duplication.
2. One-to-Few: Embed by Default
When one document owns a small, bounded number of related items that always belong to it and rarely need to be queried on their own — a user's few addresses, a product's handful of variants — embedding is almost always the right default. It's simple, it's fast to read, and there's no risk of an unbounded array.
db.users.insertOne({
name: "Meera Iyer",
email: "meera@example.com",
addresses: [
{ label: "Home", city: "Pune", zip: "411001" },
{ label: "Work", city: "Pune", zip: "411057" }
]
})
// Reading the whole thing back needs exactly one query
db.users.findOne({ email: "meera@example.com" })
The word "few" is doing real work here — a handful of addresses is fine embedded; thousands of order-history entries embedded in a user document is not (Section 3 covers exactly why).
3. One-to-Many & One-to-Squillions: When to Reference
As the "many" side grows past a small, bounded count, embedding starts causing real problems: MongoDB has a hard 16MB document size limit, an array that grows forever makes the parent document progressively slower to read and rewrite, and you lose the ability to query or paginate the related items on their own.
// A popular post could have thousands of comments -- reference, don't embed
db.comments.insertOne({
postId: ObjectId("post1"),
user: "dev_ana",
text: "This finally clicked for me!",
postedAt: new Date()
})
// Index the foreign-key-style field, then query comments independently
db.comments.createIndex({ postId: 1 })
db.comments.find({ postId: ObjectId("post1") }).sort({ postedAt: -1 }).limit(20)
One-to-squillions — a relationship that could genuinely reach millions of related items, like every sensor reading a single IoT device has ever produced — always references, and usually needs the "many" side's own index strategy from Week 2 to stay queryable at that scale. A useful rule of thumb: if you can't put a realistic upper bound on how large the "many" side could grow, don't embed it.
An insert or update that would push a document past 16MB fails outright — there's no soft warning first. An ever-growing embedded array (comments, log entries, order history) is the single most common way an application accidentally runs into this limit in production.
4. Hybrid Patterns: Embedding a Denormalized Copy
Real schemas frequently do both at once — reference for the source of truth, but embed a small, denormalized copy of the fields you actually display together, so your most common read still needs only one query.
// authors collection -- the source of truth
{ "_id": ObjectId("a1"), "name": "Priya Nair", "bio": "...", "email": "priya@example.com", "postCount": 42 }
// posts collection -- embeds just enough of the author to render a post list
// without a $lookup for every single listing page
{
"_id": ObjectId("post1"),
"title": "Getting Started with Aggregation",
"author": { "_id": ObjectId("a1"), "name": "Priya Nair" }
}
The cost of this pattern is explicit: if an author changes their display name, every post document embedding the old copy is now stale until you update it. That's a deliberate tradeoff, not an accident — you're trading a small amount of write-time complexity (updating denormalized copies when the source changes) for read-time simplicity on your most common query. Choose it when reads vastly outnumber writes to the duplicated field, which is true for most display data.
5. Enforcing Structure with $jsonSchema Validation
"Schemaless" doesn't mean you can't enforce structure — $jsonSchema
validation lets a collection reject documents that don't match a shape you
define, giving you database-level guardrails on top of application-level
discipline.
db.createCollection("products", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "price", "category"],
properties: {
name: { bsonType: "string", description: "must be a string and is required" },
price: { bsonType: "number", minimum: 0, description: "must be a non-negative number" },
category: { enum: ["Electronics", "Office", "Home"], description: "must be one of the allowed categories" }
}
}
}
})
// This insert is rejected -- price is negative and category isn't in the enum
db.products.insertOne({ name: "Broken Widget", price: -5, category: "Toys" })
Validation runs on every insert and update by default, and you can add it to an
existing collection with collMod rather than only at creation time.
This is the same instinct as strict typing in application code — catching a
malformed document at write time is far cheaper than discovering it while
reading corrupted data back weeks later.
6. Hands-on Exercise
Model a blog with the right pattern for each relationship
Design and build a small blog schema, deliberately using a different pattern for each of its three relationships.
Requirements:
- Create an
authorscollection. Each author should embed a small, boundedsocialLinksarray (one-to-few) directly on the document. - Create a
postscollection where each post embeds a denormalized{ _id, name }copy of its author (the hybrid pattern), not the full author record. - Create a separate
commentscollection referencing posts bypostId(one-to-many/squillions) — insert at least 15 comments across 2–3 posts. - Index
comments.postId, and write a query returning the 5 most recent comments for one post. - Add
$jsonSchemavalidation topostsrequiringtitle(string) andauthor._id(present), and confirm an insert missingtitleis rejected. - Write two or three sentences justifying why you chose embedding for the author's social links but referencing for comments — tie the answer back to Section 1's "together or separately" question.
If your validator rejects every insert, including ones you expect to succeed, double-check required lists exact field names and that nested fields (like author._id) need properties nesting to validate, not a flat dotted-path string.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What's the single question this lesson recommends asking to decide between embedding and referencing?
What's the single question this lesson recommends asking to decide between embedding and referencing?
"Do I usually need this data together, or separately?" — model for your application's actual read patterns rather than starting from the data's abstract structure the way relational normalization does.
Q2
Why is embedding a blog post's comments directly into the post document a risky choice for a popular blog?
Why is embedding a blog post's comments directly into the post document a risky choice for a popular blog?
Comments on a popular post could grow into the thousands with no natural upper bound — an unbounded embedded array risks hitting MongoDB's hard 16MB document size limit, and makes the parent post document progressively slower to read and rewrite as it grows. This is a one-to-squillions relationship, which should be referenced, not embedded.
Q3
In the hybrid pattern, what's the real cost of embedding a denormalized copy of an author's name on every post?
In the hybrid pattern, what's the real cost of embedding a denormalized copy of an author's name on every post?
If the author's name changes, every post document holding the old embedded copy becomes stale until it's explicitly updated — the denormalized copy isn't automatically kept in sync. This is a deliberate tradeoff: extra write-time work to keep copies current, in exchange for not needing a $lookup on every read of a post list.
Q4
Does $jsonSchema validation run only when a collection is first created?
Does $jsonSchema validation run only when a collection is first created?
No — once set, a validator runs on every insert and update against that collection, rejecting documents that don't match. It can also be added to an already-existing collection later using collMod, rather than only being available at collection-creation time.