Filter concepts by levelShowing all levels.

MongoDB · Schema Design Patterns

Schema Design Patterns

The six named patterns worked in detail, time-series bucketing, event history, and precomputed summaries

  • Six patternseach targets one specific, measured symptom
  • Bucket width = query widthmatch the interval to the dominant query range
  • History is unboundedmodel it as its own collection, not a growing array
  • Precompute + reconcile$inc on write, a periodic job to catch drift

1.The six patterns, worked in detail

Subset keeps only a needed slice of a large sub-document embedded. Bucket groups many small readings into fewer per-interval documents. Extended reference copies a few fields to skip a $lookup. Computed stores a value instead of recalculating it. Attribute turns sparse fields into a queryable array. Outlier handles the rare oversized document separately.

Key features

  • Subset: embed only the N most-needed sub-items (e.g. 10 latest reviews), reference the rest
  • Bucket: group readings by interval (e.g. one document per sensor per hour) instead of one document per reading
  • Extended reference: copy a few stable, frequently-needed fields onto the referencing document
  • Computed: store a derived value and update it incrementally, instead of recalculating on every read
  • Attribute: move sparse or varying fields into a { k, v } array so one index covers all of them
  • Outlier: detect and handle the rare huge document (a celebrity account, a viral post) as a special case

Pattern → symptom it targets → what changes

Pattern → symptom it targets → what changes
PatternTargetsWhat changes
Subsetworking set too large to fit in memoryembed a bounded slice, reference the full set
Buckettoo many small documents, high per-doc overheadgroup into fewer, larger interval documents
Extended referencea $lookup on every read of a hot pathcopy the needed fields, skip the join
Computedthe same expensive calculation runs on every readstore the result, update it on write
Attributemany sparse or varying field names, hard to indexname/value pairs in one indexed array
Outlierone document type breaks the design for the restspecial-case the outlier, keep the common design simple
Symptom first, then the matching pattern

measured symptom

e.g. slow read, huge working set

matching pattern

one of the six

targeted fix

not a general rewrite

  1. measured symptom — e.g. slow read, huge working set
  2. matching pattern — one of the six
  3. targeted fix — not a general rewrite

Command

text
// Each pattern is applied to the specific collection/field showing the symptom — not adopted project-wide

Subset, bucket, extended reference, computed, attribute, outlier — six patterns, each targeting one specific, measurable symptom. Apply one at a time, against a measured cost, not speculatively.

2.How bucket boundaries affect update and query cost

A bucket's time span sets a direct trade: wider buckets mean fewer documents but each write touches a bigger document, and a narrow query range can still pull in an oversized bucket. Narrower buckets mean smaller, cheaper individual writes but more documents to scan for a wide query.

Key features

  • Wider buckets: fewer documents, but each append/update touches more data
  • Narrower buckets: cheaper individual writes, but more documents for a wide-range query to open
  • A query narrower than the bucket width still pays to load the whole bucket document
  • A query wider than the bucket width has to merge results across several bucket documents
  • Bucket width should match the width of the app's dominant query range, not a fixed default

Command

text
// Choose bucket width ≈ the width of the app's most common query range, not the finest or coarsest possible interval

Bucket width trades write cost against document count. Match it to the width of the app's dominant query range — a mismatched width costs on every read or every write, whichever direction it's off.

3.Modeling event and history data without giant documents

Event and history data (logs, audit trails, activity feeds) is a textbook unbounded array — it grows for the life of its parent. Model it as its own collection, one document per event, referencing the parent by id, rather than appending to an array on the parent forever.

Key features

  • Event/history data is unbounded by nature — nothing in the domain caps how many events accumulate
  • Model it as its own collection: one document per event, with a parent id and a timestamp
  • Index the parent id (and often the timestamp) for the common "history for this one parent" query
  • The subset pattern applies when the parent document still wants to show a recent-events preview
  • This is the same reasoning as bounded-vs-unbounded arrays, applied specifically to logs/audit/activity data

Command

text
{ _id, parentId, type, at: ISODate(...), ...eventFields }  // one document per event, in its own collection

Event/history data is unbounded by nature — model it as its own collection (parent id + timestamp, indexed), not as a field that grows on the parent forever.

4.Precomputing counters and summaries

The computed pattern applied specifically to counts and summaries: instead of running an aggregation over many documents on every read (e.g. "how many likes does this post have"), maintain the number as a field, updated incrementally with $inc on every write that changes it.

Key features

  • Precompute when a value is read far more often than the underlying data changes
  • Maintain it with atomic increment operators ($inc), not by re-aggregating and overwriting
  • The precomputed value can drift if a write path updates the source data but forgets to update the counter
  • A periodic reconciliation job (re-aggregate and compare) catches drift that atomic increments alone cannot
  • Not every summary needs precomputing — only ones where the read cost is actually measured as expensive

Aggregate-on-read vs. precomputed

Aggregate-on-read vs. precomputed
ApproachRead costWrite costDrift risk
Aggregate on every readscans/counts related documents each timenone — no extra writenone — always exact
Precomputed counterone field readone $inc per relevant writepossible if a write path is missed

Command

text
db.<collection>.updateOne(<filter>, { $inc: { <counterField>: 1 } })  // atomic, safe under concurrent writes

Precompute a counter/summary when reads vastly outnumber the writes that change it — maintain it with atomic $inc, and add periodic reconciliation to catch drift from any write path that misses the increment.

Remember

  • Subset, bucket, extended reference, computed, attribute, outlier — apply one at a time, against a measured symptom.
  • A bucket wider than the dominant query range over-fetches; narrower than it forces scanning many documents.
  • Event/history/audit data is unbounded by nature — its own collection, indexed by parent id and time.
  • A precomputed counter needs both an atomic $inc on every relevant write and periodic reconciliation.

Pro tip

  • Apply patterns one at a time against a real measured cost — stacking several speculatively multiplies maintenance for unproven benefit.
  • The subset pattern can coexist with an unbounded event collection: embed a small recent-events preview, reference the rest.

Next up

  • Schema Validation
  • Indexing Fundamentals
  • Compound Index Design

Sources

  • Building with Patterns — mongodb.com/blog/post/building-with-patterns-a-summary
  • Schema design patterns — mongodb.com/docs/manual/data-modeling/design-patterns/
  • Time series collections — mongodb.com/docs/manual/core/timeseries-collections/
  • Checked 2026-08-21

Spotted a mistake on this page? Tell us.