Data Engineering  /  Data Modeling

๐Ÿงฑ Data Modeling 20 guides ยท updated 2026

From ER diagrams to data mesh โ€” relational, dimensional, NoSQL, and governance-driven modeling for building data platforms people can trust.

Document Database Design: Schema Patterns for MongoDB and Beyond

โ€œSchemalessโ€ is the most misleading word in databases. A document store like MongoDB wonโ€™t enforce a schema, but your application depends on one with every field it reads โ€” the schema just moved from the database into your code, where itโ€™s invisible and unversioned unless you design deliberately. Teams that treat document databases as a place to throw arbitrary JSON end up with the worst of both worlds: relational rigidity in the code and chaos in the data.

This post covers how to design document schemas on purpose: finding aggregate boundaries, the pattern catalog that experienced MongoDB teams rely on, indexing, and managing schema change. It assumes youโ€™ve read the NoSQL modeling overview in this series (design from access patterns, embed vs. reference).

The Core Unit: The Aggregate

The right mental model for a document isnโ€™t โ€œa row with flexible columnsโ€ โ€” itโ€™s an aggregate: a cluster of data that the application treats as one unit, reads together, and updates together. The design question for every document collection is: where does the aggregate boundary sit?

A blog is the classic teaching example because it has one of everything. Consider posts, comments, authors, and tags:

{
"_id": "post_481",
"title": "Why our migration took a year",
"author": { "id": "u_12", "name": "Asha Patel" },
"tags": ["postgres", "migrations"],
"comment_count": 214,
"recent_comments": [
{ "user": "u_87", "text": "Great writeup", "at": "2026-06-30T10:12:00Z" }
]
}

This document demonstrates the answer most mature designs converge on for the comments dilemma: the subset pattern โ€” embed the handful you actually display (the most recent five), keep the full set in a separate comments collection, and maintain a comment_count. One read renders the page; the โ€œview all 214 commentsโ€ click pays for the second query only when a user actually asks for it.

The Pattern Catalog

A handful of named patterns solve most recurring document-design problems. Knowing them by name shortcuts a lot of rediscovery:

Extended reference. When referencing another document, embed the few fields you need for display alongside the ID โ€” author: {id, name} above โ€” so the common read avoids a second lookup. The embedded fields are a mirror; decide how they refresh (background job, on-write fan-out, or โ€œstale is fineโ€).

Subset. As above: embed the top-N of a large related set, store the remainder separately. Fits comments, reviews, order history previews.

Bucketing. For high-volume series data (metrics, readings, messages), one document per event creates millions of tiny documents, while one document per device grows unboundedly. The middle path: one document per device per hour/day, with readings in an array. Bounded documents, few of them, range-readable. (Time-series modeling gets a dedicated post in this series.)

Computed pattern. Store precomputed aggregates (comment_count, avg_rating) updated on write, instead of recalculating on every read. This is materialized-view thinking applied inside a document.

Polymorphic collection. Different-but-related shapes in one collection, distinguished by a type field: a notifications collection holding email, SMS, and push variants. Works because document stores donโ€™t force uniform columns; keep the shared core of fields consistent so queries across types stay sane.

Schema versioning. Add a schema_version field from day one. When the shape changes, new writes carry v: 2 and readers handle both until a lazy migration completes. More on this below.

What Not to Do

The unbounded array. The single most common production incident in document databases: an embedded array (events, logs, followers) that grows until documents hit the 16 MB cap or every update rewrites megabytes. Rule: embed only what has a natural bound. โ€œFollowersโ€ does not.

Relational cosplay. Fifteen collections of tiny normalized documents joined with $lookup everywhere. Youโ€™ve rebuilt a relational database on an engine without a query planner built for joins. If your design is dominated by references, thatโ€™s a signal the workload wanted a relational store.

One giant everything-document. The opposite failure: customer + all orders + all tickets + all preferences in one document. Every reader loads everything, every writer contends on one document, and the working set balloons. Aggregate boundaries exist to be boundaries.

Ignoring atomicity boundaries. In MongoDB, a single-document update is atomic; multi-document transactions exist but cost more and are frequently a design smell. Shape aggregates so that invariants you must protect (โ€œorder total equals sum of linesโ€) live inside one document. If your invariants keep spanning documents, revisit the boundaries.

Indexing: The Half of Design Everyone Defers

Document flexibility doesnโ€™t exempt you from index discipline โ€” an unindexed query scans the whole collection exactly like any database.

Run explain() on your top queries before launch. โ€œCOLLSCANโ€ on a large collection is the same emergency a Seq Scan is in PostgreSQL.

Schema Evolution Without Downtime

Because the database doesnโ€™t enforce shape, you must manage change. The battle-tested recipe:

  1. Version every document (schema_version: 1).
  2. Write new shape, read both. Deploy readers that handle v1 and v2, then start writing v2.
  3. Migrate lazily. Upgrade documents on touch (when read/written), with an optional background sweep for the long tail.
  4. Retire the old reader once the sweep confirms no v1 remains.

This avoids the โ€œmigrate 200 million documents in one maintenance windowโ€ cliff that big-bang migrations create. MongoDB also supports optional JSON Schema validation per collection โ€” turning it on in moderate mode gives you guardrails for new writes without breaking legacy documents. Flexible schema is a tool for evolving deliberately, not a permission slip to stop designing.

A Decision Checklist

Before shipping a collection design, answer these in writing:

  1. What are the top five queries, and how many document reads does each one cost? (Target: one for the hot paths.)
  2. Is every embedded array bounded? Whatโ€™s the realistic maximum?
  3. For every duplicated field: snapshot or mirror? What refreshes mirrors?
  4. Do your critical invariants live within single documents?
  5. Does every query have a supporting index, verified with explain()?
  6. Is schema_version present, and does the team know the lazy-migration recipe?

Document databases shine when your data naturally clumps into aggregates that screens consume whole โ€” catalogs, profiles, orders, content. Design the clumps deliberately, bound the arrays, name your patterns, and version your shapes, and the โ€œschemalessโ€ store becomes one of the most pleasant persistence layers there is. Skip the design step, and the schema still exists โ€” itโ€™s just scattered across every service that ever wrote a document, waiting to disagree.