Why Schema Design Starts With Queries, Not Tables
In a relational database you typically normalize first, then write queries against the normalized tables. MongoDB flips this: you start by listing the queries and updates your application actually performs, and the schema is the artifact that makes those operations fast and simple. A document's shape should mirror how the application reads and writes data together, because MongoDB fetches a whole document in one round trip and has no built-in cross-collection JOIN in most CRUD paths. Two applications storing 'orders' can therefore end up with completely different document shapes if one reads by customer and the other reads by warehouse.
Cricket analogy: A captain doesn't set a fielding plan from a rulebook diagram; he sets it from how the specific batsman scores, say Virat Kohli's tendency to work the ball through square leg, so the field mirrors the actual access pattern of runs, not an abstract ideal.
Modeling Around the Read/Write Ratio
A key design lever is the ratio of reads to writes for a piece of data. Data that is read far more often than it is written, such as a product's category and brand on a catalog page, is a strong candidate for embedding or duplication, because paying an update cost occasionally is cheap compared to the savings on every read. Conversely, data that changes constantly, like a live stock count updated by every sale, should stay isolated in its own document or field so that high-frequency writes don't repeatedly rewrite a large, unrelated document and trigger document growth or lock contention.
Cricket analogy: A scoreboard operator keeps the current bowling figures on a separate, frequently updated ticker instead of re-printing the whole scorecard after every ball, exactly like isolating high-write fields such as inventory counts from a rarely-changing product document.
Avoiding Unbounded Arrays and the 16MB Document Limit
MongoDB documents have a hard 16MB size cap, and even well below that ceiling, arrays that grow without bound (comments on a post, events on a user timeline) cause practical problems: every array modification rewrites the containing document, index entries multiply per array element, and read amplification grows as you fetch a giant document just to show its first ten items. The standard mitigation is the 'outlier pattern' or 'bucket pattern' — keep a small, bounded subset embedded (e.g., the latest 10 comments) and move the unbounded tail into a separate collection referenced by the parent's _id, or pre-aggregate related events into fixed-size time-bucket documents.
Cricket analogy: A stadium's electronic scoreboard shows only the last six balls of an over, not the entire match history, because displaying everything would overflow the display; unbounded comment arrays get similarly capped and the full history moved elsewhere.
// Outlier pattern: bound the embedded array, overflow to a separate collection
db.posts.insertOne({
_id: ObjectId(),
title: "MongoDB Schema Design 101",
authorId: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"),
commentCount: 4821,
// Only the most recent 10 comments are embedded for fast page loads
recentComments: [
{ userId: ObjectId(), text: "Great breakdown!", createdAt: ISODate() },
{ userId: ObjectId(), text: "Helped me refactor our orders schema.", createdAt: ISODate() }
],
hasMoreComments: true
});
// Overflow comments live in their own collection, paginated by postId
db.comments.createIndex({ postId: 1, createdAt: -1 });
db.comments.find({ postId: ObjectId("...") }).sort({ createdAt: -1 }).limit(20);Do not model a one-to-many relationship as an ever-growing embedded array just because 'embedding is faster.' If the array can exceed a few hundred elements or grow without a natural cap, use the outlier or bucket pattern instead — otherwise you risk hitting the 16MB document limit and degrading write performance as MongoDB repeatedly reallocates space for the growing document.
A practical rule of thumb: embed when the child data is bounded, queried together with the parent, and updated at a similar frequency; reference when the child data is unbounded, queried independently, or updated far more often than the parent.
- Design the schema around actual application queries and updates, not abstract normalization rules.
- Use the read/write ratio to decide what to embed versus isolate: high-read, low-write data is a good embedding candidate.
- MongoDB enforces a hard 16MB per-document limit; unbounded arrays are a common way to approach it.
- The outlier (or bucket) pattern bounds embedded arrays and moves overflow into a referenced collection.
- Document growth from array modifications can cause repeated storage reallocation and slower writes.
- There is no single 'correct' schema for an entity — the same entity can be modeled differently across two applications with different access patterns.
- Revisit schema decisions when access patterns change; a schema optimized for yesterday's queries can become a bottleneck for today's.
Practice what you learned
1. What should primarily drive MongoDB schema design decisions?
2. Which type of field is the best candidate for embedding?
3. What is the hard document size limit in MongoDB?
4. What does the outlier (bucket) pattern do?
5. Why can unbounded array growth hurt write performance?
Was this page helpful?
You May Also Like
Embedding vs Referencing
Understand the core MongoDB modeling trade-off between embedding related data inside a document and referencing it via a separate collection.
Data Modeling Patterns
A tour of named MongoDB schema design patterns — attribute, bucket, computed, subset, and extended reference — and the problems each one solves.
Working with Arrays and Nested Documents
Master querying and updating deeply nested MongoDB documents and arrays using dot notation, positional operators, and array update operators.
Validation with JSON Schema
Enforce structural rules on flexible MongoDB collections using $jsonSchema validators, without giving up the benefits of a dynamic schema.