In a relational database, schema design follows the structure of the data: normalise it into tables so every fact is stored once. In MongoDB the guiding principle is different: design for the way your application uses the data. Data that is accessed together should be stored together, and the queries your app runs most often should be simple and fast, ideally reading a single document.
That means starting from access patterns. Which screens and API endpoints exist? What do they read and write, and how often? A product page that shows a product, its five latest reviews and its average rating suggests a very different design from a reporting tool that analyses every review ever written.
Over the years, MongoDB developers have identified recurring schema design patterns that solve common problems: storing precomputed values, keeping a small subset of a large list, grouping time series readings into buckets, mixing document shapes in one collection, and evolving documents over time. In this lesson you'll learn these patterns with concrete examples, recognise the anti-patterns that cause slow apps, and walk through the design of a small shop.
Start from access patterns
- List the operations: "show product page", "add review", "list orders for a customer", "daily sales report".
- Note how often each runs and how fast it must be. Reads usually far outnumber writes.
- Decide embed or reference for each relationship, as in the embedded documents lesson.
- Apply patterns where they help.
- Add indexes for every frequent query, and validation for important fields.
Common patterns
| Pattern | Idea | Example |
|---|---|---|
| Computed | Store a value that's expensive to calculate, update it on write | ratingAvg and reviewCount on a product |
| Subset | Embed a small, frequently used part of a large related list | The 5 newest reviews inside the product; all reviews in their own collection |
| Bucket | Group many small items into one document per time window | One document per sensor per hour holding 60 readings |
| Polymorphic | Different shapes in one collection, with a type field | Books, films and games in one products collection |
| Extended reference | Copy a few fields from a referenced document | Customer name on each order |
| Outlier | Handle rare huge cases separately | Overflow documents for the one product with 50,000 reviews |
| Schema versioning | Add schemaVersion and migrate gradually | Old documents updated when read or by a background job |
Anti-patterns to avoid
- Unbounded arrays: arrays that grow forever, like every event for a user. They hit the 16 MB limit and slow every update.
- Massive numbers of collections: one collection per customer or per day. Use one collection with a field and an index instead.
- Bloated documents: huge documents where most pages need only a few fields. Split rarely used data out, or use the subset pattern.
- Unnecessary indexes, and missing indexes on frequent queries.
- Normalising everything as if it were SQL, then joining with
$lookupon every request. - Case-sensitive lookups on user-entered text without a normalised field or collation.
MongoDB Data Modelling Syntax
Pattern shapes
// computed
{ _id: 'p1', name: 'Lamp', reviewCount: 120, ratingSum: 528, ratingAvg: 4.4 }
// subset
{ _id: 'p1', name: 'Lamp', recentReviews: [ /* 5 newest */ ] }
// bucket
{ sensor: 's1', hour: ISODate('2026-03-01T10:00:00Z'), count: 60, readings: [ /* ... */ ] }
// polymorphic
{ type: 'book', title: 'Dune', pages: 412 }
{ type: 'film', title: 'Alien', minutes: 117 }
// schema versioning
{ schemaVersion: 2, name: { first: 'Ada', last: 'Lovelace' } }MongoDB Data Modelling Summary
- Design MongoDB schemas from access patterns: what's read together is stored together.
- Computed, subset, bucket, polymorphic, extended reference, outlier and versioning are common patterns.
- Avoid unbounded arrays, collection-per-entity designs, bloated documents and joins on every request.
- Trade some write work and duplication for fast, simple reads.
- Support every frequent query with an index, and validate important fields.
MongoDB Data Modelling Examples
Each example shows the code, the output it produces and what each part does.
The computed and subset patterns
db.products.insertOne({ _id: 'lamp', name: 'Desk lamp', reviewCount: 0, ratingSum: 0, recentReviews: [] })
function addReview(productId, rating, text) {
db.reviews.insertOne({ productId, rating, text })
db.products.updateOne({ _id: productId }, {
$inc: { reviewCount: 1, ratingSum: rating },
$push: { recentReviews: { $each: [{ rating, text }], $position: 0, $slice: 2 } }
})
}
addReview('lamp', 5, 'Bright and sturdy')
addReview('lamp', 3, 'Base wobbles')
addReview('lamp', 4, 'Good value')
const p = db.products.findOne({ _id: 'lamp' })
print(`${p.reviewCount} reviews, average ${(p.ratingSum / p.reviewCount).toFixed(1)}`)
p.recentReviews
db.reviews.countDocuments({ productId: 'lamp' }){ acknowledged: true, insertedId: 'lamp' }
3 reviews, average 4.0
[
{ rating: 4, text: 'Good value' },
{ rating: 3, text: 'Base wobbles' }
]
3Line-by-line explanation
- Line 4Every review is stored in its own collection, so the full list can grow without limit.
- Line 6The computed pattern keeps a running count and total, so the average never needs recalculating from all reviews.
- Line 7The subset pattern keeps only the two newest reviews inside the product; $position: 0 adds at the front.
- Lines 15 to 16The product page needs one small document for its rating and latest reviews.
The bucket pattern for sensor readings
function record(sensor, at, value) {
const hour = new Date(at)
hour.setUTCMinutes(0, 0, 0)
db.readings.updateOne(
{ sensor, hour, count: { $lt: 60 } },
{ $push: { values: { at, value } }, $inc: { count: 1, sum: value } },
{ upsert: true }
)
}
for (let m = 0; m < 90; m++) {
record('s1', new Date(Date.UTC(2026, 2, 1, 10, 0) + m * 60000), 20 + (m % 5))
}
db.readings.countDocuments()
db.readings.find({}, { _id: 0, hour: 1, count: 1, avg: { $divide: ['$sum', '$count'] } }).sort({ hour: 1 })2
[
{ hour: ISODate('2026-03-01T10:00:00.000Z'), count: 60, avg: 22 },
{ hour: ISODate('2026-03-01T11:00:00.000Z'), count: 30, avg: 22 }
]Line-by-line explanation
- Lines 2 to 3Each reading is assigned to the bucket for its hour.
- Line 5A bucket accepts up to 60 readings, so documents stay bounded.
- Line 6Each reading is pushed into the bucket, and running totals are updated in the same write.
- Line 7upsert creates the next bucket automatically.
- Line 15Ninety readings are stored in just two documents, instead of ninety.
- Line 16Hourly averages come straight from the stored count and sum. MongoDB's time series collections apply this idea automatically.
Polymorphic documents and schema versions
db.catalogue.insertMany([
{ _id: 1, type: 'book', title: 'Dune', price: 9.99, pages: 412 },
{ _id: 2, type: 'film', title: 'Alien', price: 7.99, minutes: 117 },
{ _id: 3, type: 'game', title: 'Tetris', price: 4.99, players: 1 }
])
db.catalogue.find({ price: { $lt: 9 } }, { _id: 0, type: 1, title: 1 })
db.catalogue.find({ type: 'book' }, { _id: 0, title: 1, pages: 1 })
db.people.insertMany([
{ _id: 1, name: 'Ada Lovelace' },
{ _id: 2, schemaVersion: 2, name: { first: 'Grace', last: 'Hopper' } }
])
db.people.updateMany({ schemaVersion: { $exists: false } }, [
{ $set: {
schemaVersion: 2,
name: {
first: { $arrayElemAt: [{ $split: ['$name', ' '] }, 0] },
last: { $arrayElemAt: [{ $split: ['$name', ' '] }, 1] }
}
} }
]).modifiedCount
db.people.find({}, { _id: 0 }){ acknowledged: true, insertedIds: { '0': 1, '1': 2, '2': 3 } }
[ { type: 'film', title: 'Alien' }, { type: 'game', title: 'Tetris' } ]
[ { title: 'Dune', pages: 412 } ]
{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }
1
[
{ name: { first: 'Ada', last: 'Lovelace' }, schemaVersion: 2 },
{ schemaVersion: 2, name: { first: 'Grace', last: 'Hopper' } }
]Line-by-line explanation
- Lines 2 to 4One collection holds different kinds of product, sharing common fields like title and price.
- Line 6Queries on shared fields work across every type.
- Line 7The type field selects one kind, with its own fields.
- Lines 9 to 10Old documents store a single name; newer ones, marked schemaVersion 2, split it.
- Lines 12 to 19A migration upgrades old documents to version 2 with an update pipeline.
Common MongoDB Data Modelling Mistakes
One collection per customer
db.orders_customer_1042.insertOne(order) // thousands of collections, hard to query and indexdb.orders.insertOne({ customerId: 1042, ...order })
db.orders.createIndex({ customerId: 1 })Recalculating on every read
db.reviews.aggregate([{ $match: { productId } }, { $group: { _id: null, avg: { $avg: '$rating' } } }]) // on every product viewdb.products.findOne({ _id: productId }, { ratingAvg: 1, reviewCount: 1 }) // computed patternDesigning tables first
// products, product_images, product_prices and product_tags as four collections joined on every page{ _id: 'p1', name: 'Lamp', images: [...], price: 24, tags: ['home'] }MongoDB Data Modelling in Real Projects
Patterns in production systems
Retailers use the computed pattern for ratings and stock totals, and the subset pattern for recent reviews and top comments. IoT and monitoring systems store readings in buckets or time series collections. Content platforms keep articles, videos and podcasts in one polymorphic collection. Long-lived apps add schemaVersion fields and migrate documents lazily, which lets them change structure without downtime.
MongoDB Data Modelling FAQ
Does MongoDB need a schema?
MongoDB doesn't force one, but every real application has one: the shape your code expects documents to have. The difference is that you design it around your queries rather than around tables, and you choose how strictly to enforce it with schema validation. Leaving it entirely undesigned usually ends with inconsistent fields and slow queries.
How do I model a many-to-many relationship in MongoDB?
Store an array of ids on one or both sides, such as courseIds on each student and, if you also query the other way, studentIds on each course. Keep the arrays on the side where they stay small, and index them for lookups. If the relationship itself has data, such as an enrolment date and grade, give it its own collection with one document per pair.
Should I use one collection or several for different types of items?
If the items share most fields and are queried together, such as books, films and games in one shop, use one collection with a type field: the polymorphic pattern. Use separate collections when the items are unrelated, queried separately or have very different indexes. Avoid one collection per customer or per day, which multiplies collections and indexes without benefit.
How do I change my MongoDB schema after the application is live?
Add a schemaVersion field and make your code handle both the old and new shapes. Then migrate documents gradually, either when each one is next read or written, or with a background job that updates batches with updateMany. Once every document is on the new version, remove the old-format code and tighten validation.
What is the bucket pattern in MongoDB?
The bucket pattern groups many small, related items into one document, such as all readings from one sensor in one hour. This cuts the number of documents and index entries dramatically and makes range queries cheap. For measurement data, time series collections apply this pattern automatically, so you can insert one reading per document and let MongoDB handle the bucketing.
MongoDB Data Modelling Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the rule of MongoDB schema design.
// Data that is accessed together should be together.
What does the computed pattern store?
Which pattern keeps the five newest reviews inside a product?
Why group sensor readings into hourly bucket documents?
What's the purpose of a schemaVersion field?
MongoDB Data Modelling Quiz
5 questions to check what you learned.
What should MongoDB schema design start from?
- An ER diagram in third normal form
- The number of collections
- The application's access patterns: what it reads and writes, and how often
- The size of the server
Which is an anti-pattern?
- Embedding an address in a user
- An array that grows forever, such as every event a user triggers
- A type field in a polymorphic collection
- An index on a frequent query
What does the polymorphic pattern allow?
- Different document shapes in one collection, distinguished by a type field
- Documents without _id
- Multiple databases
- Automatic joins
What's a reasonable trade-off in MongoDB schema design?
- Making every read do several joins
- Never duplicating anything
- Storing everything in one document
- Doing a little more work on writes so frequent reads are fast and simple
What does the outlier pattern handle?
- Average documents
- Deleted documents
- Rare cases that would make a document far larger than normal, by moving the extra data elsewhere
- Indexes