Schema Design Patterns

Design MongoDB schemas around how your application reads and writes data: apply the computed, subset, bucket, polymorphic, outlier and schema versioning patterns, avoid common anti-patterns, and work through a real e-commerce design.

Lesson 15 of 43Intermediate17 min read

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

  1. List the operations: "show product page", "add review", "list orders for a customer", "daily sales report".
  2. Note how often each runs and how fast it must be. Reads usually far outnumber writes.
  3. Decide embed or reference for each relationship, as in the embedded documents lesson.
  4. Apply patterns where they help.
  5. Add indexes for every frequent query, and validation for important fields.

Common patterns

PatternIdeaExample
ComputedStore a value that's expensive to calculate, update it on writeratingAvg and reviewCount on a product
SubsetEmbed a small, frequently used part of a large related listThe 5 newest reviews inside the product; all reviews in their own collection
BucketGroup many small items into one document per time windowOne document per sensor per hour holding 60 readings
PolymorphicDifferent shapes in one collection, with a type fieldBooks, films and games in one products collection
Extended referenceCopy a few fields from a referenced documentCustomer name on each order
OutlierHandle rare huge cases separatelyOverflow documents for the one product with 50,000 reviews
Schema versioningAdd schemaVersion and migrate graduallyOld 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 $lookup on 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.

Example 1

The computed and subset patterns

MongoDB Shell
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' })
Output
{ acknowledged: true, insertedId: 'lamp' }
3 reviews, average 4.0
[
  { rating: 4, text: 'Good value' },
  { rating: 3, text: 'Base wobbles' }
]
3
Line-by-line explanation
  1. Line 4Every review is stored in its own collection, so the full list can grow without limit.
  2. Line 6The computed pattern keeps a running count and total, so the average never needs recalculating from all reviews.
  3. Line 7The subset pattern keeps only the two newest reviews inside the product; $position: 0 adds at the front.
  4. Lines 15 to 16The product page needs one small document for its rating and latest reviews.
Example 2

The bucket pattern for sensor readings

MongoDB Shell
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 })
Output
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
  1. Lines 2 to 3Each reading is assigned to the bucket for its hour.
  2. Line 5A bucket accepts up to 60 readings, so documents stay bounded.
  3. Line 6Each reading is pushed into the bucket, and running totals are updated in the same write.
  4. Line 7upsert creates the next bucket automatically.
  5. Line 15Ninety readings are stored in just two documents, instead of ninety.
  6. Line 16Hourly averages come straight from the stored count and sum. MongoDB's time series collections apply this idea automatically.
Example 3

Polymorphic documents and schema versions

MongoDB Shell
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 })
Output
{ 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
  1. Lines 2 to 4One collection holds different kinds of product, sharing common fields like title and price.
  2. Line 6Queries on shared fields work across every type.
  3. Line 7The type field selects one kind, with its own fields.
  4. Lines 9 to 10Old documents store a single name; newer ones, marked schemaVersion 2, split it.
  5. Lines 12 to 19A migration upgrades old documents to version 2 with an update pipeline.

Common MongoDB Data Modelling Mistakes

One collection per customer

Don't
db.orders_customer_1042.insertOne(order)       // thousands of collections, hard to query and index
Do
db.orders.insertOne({ customerId: 1042, ...order })
db.orders.createIndex({ customerId: 1 })

Recalculating on every read

Don't
db.reviews.aggregate([{ $match: { productId } }, { $group: { _id: null, avg: { $avg: '$rating' } } }])   // on every product view
Do
db.products.findOne({ _id: productId }, { ratingAvg: 1, reviewCount: 1 })   // computed pattern

Designing tables first

Don't
// products, product_images, product_prices and product_tags as four collections joined on every page
Do
{ _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.

Fill in the blankseasy

Complete the rule of MongoDB schema design.

// Data that is accessed together should be  together.
Multiple choiceeasy

What does the computed pattern store?

Multiple choicemedium

Which pattern keeps the five newest reviews inside a product?

Multiple choicemedium

Why group sensor readings into hourly bucket documents?

Multiple choicehard

What's the purpose of a schemaVersion field?

MongoDB Data Modelling Quiz

5 questions to check what you learned.

  1. 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
  2. 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
  3. What does the polymorphic pattern allow?

    • Different document shapes in one collection, distinguished by a type field
    • Documents without _id
    • Multiple databases
    • Automatic joins
  4. 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
  5. 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