MongoDB removes the requirement that every document share an identical shape. It does not remove the consequences of shape. A document model still decides which reads are cheap, which updates are atomic, and which data can grow without bound.
Design from access patterns
Suppose a product page always needs a product and its five latest reviews. Embedding a bounded review preview can make that page one read:
{
_id: 42,
name: "Mechanical Keyboard",
price: 129,
recentReviews: [
{ userId: 7, rating: 5, text: "Solid." }
]
}
The full review history can still live in a separate collection.
In plain terms: store together what the application normally reads together—but only when the embedded part has a clear size and ownership boundary.
Embed when the child belongs to the parent
Embedding fits when related data:
- is normally read with the parent;
- is updated with the parent;
- has bounded growth;
- does not need an independent lifecycle.
One document write is atomic, which can remove cross-document coordination.
Reference when growth or ownership separates
References fit many-to-many relationships, children queried independently, frequently changing shared data, and arrays that could grow indefinitely.
// reviews collection
{ _id: 9001, productId: 42, userId: 7, rating: 5, text: "Solid." }
Embedding every review would make the product document grow continually and rewrite a larger document on each addition. MongoDB documents also have a 16 MiB size limit.
Duplication is a design tradeoff, not a free optimization. If a product name is copied into ten thousand order snapshots, decide whether it is historical truth or data that must be synchronized.
Index the query shape
db.reviews.createIndex({ productId: 1, createdAt: -1 })
db.reviews.find({ productId: 42 })
.sort({ createdAt: -1 })
.limit(5)
.explain("executionStats")
Check the winning plan, totalKeysExamined, totalDocsExamined, and returned count. A low execution time on a tiny collection proves little.
Indexes on arrays become multikey indexes. They are powerful, but wide or multiple arrays can generate many index entries. Index size and write cost still matter.
Flexible does not mean unvalidated
Use schema validation for required fields and types at stable boundaries. Allow deliberate variation, not accidental drift such as price being a number in one document and a string in another.
Decision table
| Condition | Prefer |
|---|---|
| Read and update together, bounded child set | Embed |
| Many-to-many or independent child lifecycle | Reference |
| Shared value changes frequently | Reference or explicit synchronization |
| Historical snapshot must not change | Deliberate duplication |
| Array grows without a known bound | Separate collection |
Review card
- Document shape is an access-path decision.
- Embed bounded data with shared ownership.
- Reference independent or unbounded data.
- Name the consistency rule for every duplicate.
- Validate stable fields even in a flexible model.
- Use
explain("executionStats"), not intuition, to verify indexes.