Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,067visitors

Built for engineers who want to go deep.


← Spring Data MongoDB

MongoDB Basics

  • Spring Data MongoDB
  • Indexing & Performance

Aggregation Pipeline

  • Aggregation Pipeline
  • Transactions in MongoDB
  • Schema Design Patterns
Chaturmind
← Spring Data MongoDB

MongoDB Basics

  • Spring Data MongoDB
  • Indexing & Performance

Aggregation Pipeline

  • Aggregation Pipeline
  • Transactions in MongoDB
  • Schema Design Patterns
HomeLearnSpring BootSpring Data MongoDBAggregation Pipeline
✓ FreeAdvanced· 7 min read

Schema Design Patterns

Embedding vs referencing, polymorphic patterns, bucket pattern — design documents right.

Published September 21, 2026


Schema Design Patterns

MongoDB doesn't force a schema, but every application has one: it's implied by how your code reads and writes documents. A good MongoDB schema isn't derived from the entity–relationship diagram, as in a relational design. It's derived from the access patterns: which data is read together, how often, how it grows, and which operations must be atomic.

The guiding rule is: data that is accessed together should be stored together, as long as the document stays bounded in size.

Advertisement

Embedding vs referencing

Embedding puts related data inside the parent document:

{
  "_id": "u_1",
  "name": "Asha",
  "addresses": [
    { "label": "home", "city": "Pune",   "pin": "411001" },
    { "label": "work", "city": "Mumbai", "pin": "400051" }
  ]
}
  • ✅ One read returns everything, with no $lookup.
  • ✅ Updates to one document are atomic, so the user and their addresses change together.
  • ❌ Documents are limited to 16 MB, and large, growing documents are slow to read and rewrite.
  • ❌ Duplicated data (for example, an author name embedded in many posts) must be updated everywhere.

Referencing stores an ID and fetches the related document separately:

{ "_id": "o_981", "userId": "u_1", "total": 129.49, "status": "PAID" }
  • ✅ Unbounded or independently accessed data stays manageable.
  • ✅ Shared entities are stored once.
  • ❌ Reading related data needs a second query or an aggregation $lookup.

How to decide:

Relationship / accessUsually
One-to-few, always read with the parent (addresses, phone numbers)Embed
One-to-many, bounded, read together (order → line items)Embed
One-to-many, large or growing without limit (post → comments, user → orders)Reference (child stores parent ID)
Many-to-many (students ↔ courses)Reference, often arrays of IDs on the smaller side
Data needed independently, or shared by many parentsReference
Data that changes much more often than its parentOften reference

Unbounded arrays are the classic anti-pattern. An array that grows forever (all comments, all log events) eventually hits the size limit, makes every update rewrite a large document, and bloats indexes.

Pattern: extended reference (controlled duplication)

Store a reference plus a few frequently needed fields from the referenced document:

{
  "_id": "o_981",
  "customer": { "id": "u_1", "name": "Asha" },
  "items": [ { "sku": "KB-01", "name": "Mechanical Keyboard", "price": 89.99, "qty": 1 } ]
}

Order lists can now show the customer name and item names without a join. Choose fields that rarely change, or whose historical value is actually correct. The price at purchase time should be frozen on the order.

Pattern: subset

Keep the most recent or most relevant few children embedded, and the rest in a separate collection. A product page shows the latest 10 reviews embedded in the product document, and "see all reviews" queries the reviews collection. The document stays small, and the common read stays a single fetch.

Pattern: bucket (time series)

Instead of one document per measurement, group measurements into a bucket per entity and time window:

{
  "sensorId": "s_42",
  "start": { "$date": "2026-01-15T10:00:00Z" },
  "count": 60,
  "readings": [ { "t": 0, "temp": 22.1 }, { "t": 60, "temp": 22.3 } ],
  "sum": 1330.2, "min": 21.8, "max": 22.5
}

This means fewer documents and index entries, and range queries touch fewer documents. The precomputed sum, min and max answer summary queries without reading every reading. Insert with an upsert that $pushes into the current bucket while count < 60. For new projects, MongoDB's native time-series collections apply this bucketing automatically.

Pattern: computed

Store values that are expensive to compute and read often (review count, average rating, total spent), and update them when the underlying data changes, either in the same write with $inc or asynchronously. This trades a little write work for much cheaper reads.

Pattern: outlier

Design for the typical case, and handle the rare huge one separately. Most books have a handful of authors to embed. A few anthologies have hundreds. Embed up to a limit, then set a flag such as hasMore: true and store the overflow in another collection. Only the outliers pay the extra cost.

Pattern: polymorphic

Store different but related shapes in one collection with a discriminator field:

{ "_id": 1, "type": "BOOK",   "title": "…", "isbn": "…",    "pages": 320 }
{ "_id": 2, "type": "COURSE", "title": "…", "durationHrs": 12, "level": "ADVANCED" }

You can query shared fields across all items, such as a catalogue search on title, while each type keeps its own fields. Spring Data maps these to a class hierarchy using the stored _class type hint.

Pattern: schema versioning

Documents written at different times may have different shapes. Add a schemaVersion field and handle each version when reading, migrating lazily (rewrite a document on its next update) or with a background job:

User toUser(Document doc) {
    int version = doc.getInteger("schemaVersion", 1);
    if (version == 1) {                                     // v1 stored a single "name" string
        String name = doc.getString("name");
        int space = name.indexOf(' ');
        return space < 0 ? new User(name, "")
                         : new User(name.substring(0, space), name.substring(space + 1));
    }
    return new User(doc.getString("firstName"), doc.getString("lastName"));
}

Handle names without a space, which is a real edge case that naive split(" ")[1] code crashes on.

Validating the schema you intended

Flexible doesn't mean unvalidated. MongoDB's $jsonSchema validator can require fields and types at the database level, protecting against bugs in any client:

db.createCollection("orders", { validator: { $jsonSchema: {
  bsonType: "object",
  required: ["customer", "items", "status"],
  properties: { status: { enum: ["PENDING", "PAID", "SHIPPED", "CANCELLED"] } }
}}})

A design checklist

  1. List the top queries and writes, with their frequencies.
  2. For each relationship, decide embed or reference using bounds and access patterns.
  3. Check the worst-case document size and growth.
  4. Identify which updates must be atomic, and keep that data in one document if you can. Otherwise, use multi-document transactions, sparingly.
  5. Design indexes for the queries (see Indexes & Query Performance).
  6. Decide how duplicated fields are kept up to date.

Follow-up questions this topic invites — and their answers

Q: What's the most common MongoDB schema mistake? A: Unbounded arrays: embedding children that grow without limit, such as all comments or all events. They approach the 16 MB limit, make every write rewrite a large document, and slow down reads. Reference them, or use the subset or bucket pattern.

Q: When do you need multi-document transactions in MongoDB? A: When an operation must atomically change data that lives in separate documents, such as transferring between two accounts. Good schema design minimizes that by keeping data that changes together in one document, because single-document writes are always atomic and much cheaper.

Q: How do you model a many-to-many relationship? A: Usually with arrays of references on the side where the list is bounded (a student's enrolled course IDs), or with a separate linking collection when both sides are large. Duplicate a few display fields if listing pages need them.

Q: How is MongoDB schema design different from relational normalization? A: Relational design normalizes first and joins at read time. MongoDB design starts from access patterns and often embeds or duplicates data to serve reads in a single document, accepting the work of keeping copies in sync.

Q: How would you handle a schema change on millions of documents? A: Make the application read both old and new shapes (with a version field), write the new shape going forward, and migrate old documents lazily on update or with a throttled background job. Once everything is migrated, remove the old-shape code.

Advertisement

Previous

Transactions in MongoDB

AI Tutor

Lesson: Schema Design Patterns

Quick actions

AI responses can be inaccurate. Verify critical information.