Data Modeling: Embedded vs Referenced
Introduction
Relational databases normalize data into tables linked by foreign keys. MongoDB gives you a choice: embed related data inside one document, or reference other documents by _id. The wrong choice causes slow queries, 16 MB document limits, or painful updates. This chapter compares both approaches with blog and e-commerce examples and links to MySQL constraints for relational contrast.
Prerequisites
Query-Driven Design
Start from how the application reads and writes, not from an ER diagram alone.
Ask:
- What does the UI fetch in one request?
- How often does related data change independently?
- How large can the relationship grow (10 comments vs 100,000)?
Design for common access patterns; denormalize deliberately.
Embedding (Denormalization)
Store related records inside the parent document.
One read returns post + comments + author snapshot:
db.posts.findOne({ slug: "mongodb-modeling" })When embedding fits
| Signal | Example |
|---|---|
| One-to-few | Address on user, tags on post (small list) |
| Read together | Product with fixed specs |
| No independent lifecycle | Comments that die with the post |
| Updates are rare | Embedded author name rarely changes |
Embedding drawbacks
- Document size — unbounded
commentsarray hits 16 MB - Stale copies — author name duplicated; update many posts if user renames
- Write amplification — adding comment rewrites whole post document
Referencing (Normalization)
Store ObjectId (or business key) pointing to another collection.
Read post, then comments:
const post = db.posts.findOne({ slug: "referenced-post" })
db.comments.find({ postId: post._id }).sort({ createdAt: 1 })Or join in aggregation — chapter 13.
When referencing fits
| Signal | Example |
|---|---|
| One-to-many / many-to-many | Orders, comments at scale |
| Independent updates | User profile changes often |
| Large or growing children | Event log per user |
| Multiple parents | Tag shared across posts |
Referencing drawbacks
- Multiple round trips in app code without
$lookup - No referential integrity by default (unlike MySQL
FOREIGN KEY) - Application must clean orphans or accept stale links
Hybrid: Summary Embed + Detail Reference
Homepage shows latest 3 comments embedded; full thread in comments collection:
db.posts.updateOne(
{ slug: "referenced-post" },
{
$set: {
recentComments: [
{ author: "Bob", body: "Nice write-up.", createdAt: new Date() }
]
}
}
)Full list: db.comments.find({ postId }). Sync recentComments on new comment (app logic or change stream).
MySQL vs MongoDB Mental Model
| MySQL | MongoDB |
|---|---|
Tables + FOREIGN KEY | Collections + embed or authorId |
JOIN | $lookup or second query |
| Normal form reduces duplication | Embed for read speed; reference for scale |
Schema in CREATE TABLE | Flexible docs + optional validation — chapter 14 |
Anti-Patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
| Unbounded embed array | 16 MB, slow loads | Separate collection |
| Deep nesting (5+ levels) | Hard queries | Flatten or reference |
| Embed everything "because NoSQL" | Update nightmares | Reference mutable data |
| Reference one-to-few always | Extra queries | Embed small stable data |
| No plan for duplicate fields | Stale author names | Reference or accept + sync job |
schemaVersion Field (Migration)
When document shape evolves:
db.posts.insertOne({
schemaVersion: 2,
title: "Versioned",
slug: "versioned",
metadata: { seoTitle: "Versioned Post" }
})Application reads schemaVersion and migrates lazily on write or via batch job—no SQL-style ALTER TABLE, but still plan migrations.
Decision Checklist
- Listed top 3 read patterns for the feature
- Estimated max child count (10? 10,000?)
- Decided embed vs reference per relationship
- Planned index on reference fields (
authorId,postId) — chapter 11 - Documented stale-data rules for embedded copies
FAQ
Should I always embed for performance?
No—embed only when bounded and read together. Large or independent data belongs in its own collection.
Enforce references like foreign keys?
Use application validation + optional $jsonSchema validator. Transactions for multi-doc consistency — chapter 14.
Same as SQL denormalization?
Similar trade-off: duplicate data for read speed. MongoDB makes embedding first-class.
Graph relationships?
Many-to-many often needs a junction collection or array of IDs — Common Modeling Patterns.
Redis for nested data?
Redis stores serialized JSON strings—different layer. MongoDB persists documents; Redis caches them — Redis caching.