MongoDB data modeling in practice is a small vocabulary of patterns applied to specific problems: a join on the hot path, an array that grows, a value that is expensive to compute, documents whose shape varies. Each pattern buys a cheaper read or a smaller document and charges for it somewhere else, usually in write complexity or in data that can go stale. This catalogue gives the document shape, the query or update that makes each pattern work, and what it costs.
Most of these are described in the manual's schema design patterns section. The extended reference comes from MongoDB's "Building with Patterns" series, and tree structures have their own section of the manual.
Extended reference
An order needs the customer's name and shipping address every time it is displayed. Copy those fields into the order and keep the reference for everything else.
That update needs an index on customer._id and may touch thousands of documents. Copy only fields that rarely change. Some copies should never be updated: the shipping address on a delivered order is a record of where it went, not a cache of the customer's current address.
Subset
The inverse case: a parent with many children, where the common read needs only a few. Keep all reviews in reviews and the newest five inside the product.
$slice keeps the array bounded on every write, so the product document cannot grow. The cost is two writes per review, and an edit or deletion of a review has to be applied in both places. "Show all reviews" queries the reviews collection, which needs { productId: 1, at: -1 }.
Computed
If every product page shows a review count and an average rating, do not run $group over the reviews on each view. Maintain the numbers on write:
Store the sum and the count, not the average, because $inc is atomic and an average cannot be incremented. The application divides on read. When the computation is too heavy to do per write, recompute on a schedule and write the results back with $merge:
The cost is staleness between runs, or extra write load if maintained inline. It pays off when reads of the value far outnumber the writes that change it.
Approximation
A page view counter that takes one write per view spends most of its write capacity on a number nobody needs to the last digit. Accumulate in the application and write in steps:
One write replaces a hundred. The stored value lags by up to the step size per page and per application process, and whatever is pending is lost if the process stops. Use it for popularity counters and rough totals, never for money or stock levels.
Bucket
A customer's trade history is always shown a page at a time. One document per trade means reading 50 documents per page. A bucket stores one page per document:
The filter finds a bucket with room. If none exists, the upsert creates one, so buckets fill to 50 and a new one opens with no extra logic. Reading a page is one document:
Index { customerId: 1, openedAt: -1 } to serve that read. Paging backwards through buckets by openedAt is keyset pagination, which avoids the cost described in Why does OFFSET pagination get slower on every page?.
The cost: finding or correcting one trade means searching inside arrays, filtering across buckets needs $unwind, and two concurrent upserts can occasionally open two buckets at once, which the read logic must tolerate. For timestamped measurements, do not build this by hand. Use a time series collection, described below.
Outlier
Most books sell to a few dozen buyers and the buyer list fits in the book document. A bestseller has two million. Design for the typical case and divert the exception:
Books are created with buyerCount: 0 so that the $lt filter matches. Readers check hasOverflow and only then query the overflow collection. Typical documents stay small and fast, and the threshold prevents any document from growing without bound. The cost is two code paths for every read and write of that list, which is only worth it when the outliers are a small fraction of documents.
Attribute
Products have hundreds of possible specifications and each product uses a handful. One field per specification would need one index per field, and a collection is limited to 64 indexes. Store them as key and value pairs:
One compound multikey index serves a search on any specification. $elemMatch is required so that the key and the value are matched within the same array element. The cost: queries are less readable, v mixes types so a number stored as a string will silently not match a numeric range, and each product adds one index entry per specification. A wildcard index on a subdocument ({ "specs.$**": 1 } with specs stored as an object) is an alternative when you prefer natural field names.
Polymorphic
Cards, bank accounts and wallets are all payment methods. They are listed together and share half their fields, so they belong in one collection with a discriminator:
"All payment methods for customer 811" is one query on one index. Type-specific indexes stay small with a partial filter, but the planner only uses a partial index when the query includes the filter condition, so queries for expiring cards must include type: "card". The cost is that nothing structural stops a card document from missing its card field. Enforce that per type in a $jsonSchema validator using oneOf.
Schema versioning
Requirements change after the collection has 200 million documents. Add a version field and let both shapes coexist:
New code writes version 2 and reads both, converting version 1 in memory. Documents are upgraded when they are next written, or by a background job. No downtime and no single huge migration. The cost is application code that handles each live version, and queries or indexes on the changed field that may need to cover both locations until the old version is gone.
Archive
Orders older than five years are almost never read but sit in the same indexes as last week's. Move them:
Reusing the same cutoff for the copy and the delete is what makes this safe to rerun after a failure. On large collections, run it in date slices so that each delete is small. The active collection and its indexes shrink, which helps the working set. Archived documents should be self-contained, with the related data embedded as it was at the time, because the referenced documents may have changed or gone. The manual recommends external object storage as the cheapest destination when the data does not need to stay queryable in the cluster.
Tree structures
For a category tree, store a parent reference and let $graphLookup walk it:
Moving a subtree is one update to one document, which is the strength of parent references. Each level of the traversal is another query against connectToField, so that field must be indexed. The stage has a 100 MB memory limit, beyond which it writes temporary files to disk or returns an error if allowDiskUse is false. The from collection can be sharded only from MongoDB 5.1.
When reads dominate and the tree rarely changes, store a materialized path:
A regular expression anchored at the start of the string uses the index as a range scan, so all descendants of a node come back from one indexed query with no recursion. The cost moves to writes: relocating a subtree means rewriting the path of every node below it.
Time series collections in place of hand-rolled buckets
Sensor readings, metrics and price ticks used to be the main reason to write bucket logic. Since MongoDB 5.0 the server does it:
You insert and query one document per measurement. Internally the server groups measurements with the same metaField value into compressed buckets in a columnar format, and expires whole buckets when expireAfterSeconds passes. Set granularity to match the interval between readings from one source.
The restrictions decide whether it fits:
Updates can only match on and modify the metaField. A measurement cannot be corrected in place.
timeField and metaField are fixed at creation, and an existing collection cannot be converted.
No unique indexes, no schema validation rules and no change streams.
Documents are limited to 4 MB.
Use a time series collection for append-only measurements identified by a source and a timestamp. Keep a hand-rolled bucket for grouped data that is not a time series, or whose elements need updating.
Common mistakes
Applying patterns before measuring. Each one adds write complexity. A $lookup on a screen opened ten times a day does not need an extended reference.
Copying volatile fields. An extended reference to a stock level or an account balance is wrong almost immediately.
Forgetting the second write path. Subset and computed values need handling for edits and deletes, not only for inserts.
Using the attribute pattern for every field. Fields that every document has and that are queried by name belong at the top level with ordinary indexes.
Treating schema versioning as permanent. Finish the migration and delete the old code path, or the versions accumulate.
Bucketing data that is updated per element. Positional updates inside large arrays are awkward and rewrite a large document for a small change.
The 100 MB blocking-sort limit means your index doesn't provide the sort order. Build compound indexes with the ESR rule, order pipeline stages correctly, and know when allowDiskUse is fine.