Filter concepts by levelShowing all levels.

MongoDB · Data Modeling

Data Modeling

Workload-driven design, the embed-vs-reference decision, and the risks of unbounded growth

  • Data accessed togethershould be stored together — the core principle
  • Embed by defaultreference for growth, sharing, or independent updates
  • Bounded ≠ small todaybounded means something actively caps growth
  • 16 MiB is a hard limitunbounded arrays degrade, then fail writes outright

1.Schema design as workload-driven design

A MongoDB schema is designed around how the app reads and writes data, not around normalizing entities. The same real-world data can be modeled two very different ways depending on the workload.

Key features

  • Core principle: data accessed together should be stored together
  • The workload — patterns, frequency, latency — drives the shape, not normalization
  • The same data can be modeled differently for apps with different access patterns
  • A schema optimized for one workload can perform poorly for a different one
  • Called a "core senior skill" — it requires knowing the app, not just the data
Workload first, schema second

access patterns

what gets queried, how often

schema shape

documents, embedding, indexes

performance

the point of the exercise

  1. access patterns — what gets queried, how often
  2. schema shape — documents, embedding, indexes
  3. performance — the point of the exercise

Command

text
// Not a syntax — a design order: access patterns first, document shape second.

Design the schema around the application's actual read/write patterns first — not around normalized entities the way a relational schema would be.

2.Start from access patterns, not only entities

Before designing collections, list the specific queries the app will actually run — which fields, how often, with what else — and let those drive the shape. Entities alone don't say enough to design around.

Key features

  • An access pattern names a specific query: which fields, filtered how, with what
  • The same entity list can justify very different schemas, depending on access patterns
  • Frequency matters: a page-load query deserves more accommodation than a rare report
  • Read-to-write ratio for a pattern informs whether embedding or referencing wins
  • MongoDB's schema design process starts with "identify workload," entities come after

Entity list vs. access-pattern list, same domain

Entity list vs. access-pattern list, same domain
Question typeExampleWhat it tells you
Entity (insufficient alone)"There is a User and a Post"nothing about how they are queried together
Access pattern"Show a user's 20 most recent posts, on every profile view"frequency, sort, and shape needed — argues for embedding or a targeted index
Access pattern"Search all posts by tag, across all users, rarely"a separate, independently queryable posts collection
Design input, in order

List entities

User, Post, Comment — necessary, not sufficient

List access patterns

"profile view: user + 20 recent posts", frequency noted

Weigh by frequency

a page-load query outweighs a monthly report

Shape the schema

embed or reference follows from the patterns above

  1. List entities — User, Post, Comment — necessary, not sufficient
  2. List access patterns — "profile view: user + 20 recent posts", frequency noted
  3. Weigh by frequency — a page-load query outweighs a monthly report
  4. Shape the schema — embed or reference follows from the patterns above

Command

text
// Design input: a list of { query, frequency, latency requirement } — not just an entity-relationship diagram.

List the actual queries — fields, frequency, together-with-what — before shaping collections. Entities alone under-specify the design.

3.Workload types to identify

Six workload shapes come up repeatedly: read-heavy, write-heavy, high-cardinality, time-series, hierarchical, transactional. Naming one helps pick the right pattern instead of reasoning from scratch.

Key features

  • Read-heavy: optimize for fast, frequent reads, even at some write cost
  • Write-heavy: optimize for fast writes — smaller documents, fewer indexes to update
  • High-cardinality: a field with many distinct values — shapes indexing/sharding
  • Time-series: data ordered/queried by time — often bucketed into per-interval documents
  • Hierarchical: parent/child or tree-shaped data (categories, comment threads)
  • Transactional: operations needing atomicity/consistency across multiple documents

The six workload types

The six workload types
TypeSignalCommon modeling response
Read-heavyreads vastly outnumber writesembed/denormalize for one-query reads
Write-heavywrites vastly outnumber readskeep documents small; minimize indexes updated per write
High-cardinalityfield has many distinct valuesindex carefully; consider it for shard keys
Time-seriesdata ordered/queried by timebucket by interval; consider time-series collections
Hierarchicalparent/child, tree-shaped datamaterialized paths, tree references, or nested sets
Transactionalneeds multi-document atomicitymulti-document transactions, or model to avoid needing them
Name the workload, then reach for its pattern

six workload types

recognizable shapes

known pattern

per type

schema decision

not from scratch

  1. six workload types — recognizable shapes
  2. known pattern — per type
  3. schema decision — not from scratch

Command

text
// Not a syntax — a classification exercise applied per access pattern, before choosing embed/reference/index shape.

Six workload types — read/write-heavy, high-cardinality, time-series, hierarchical, transactional — apply per pattern, not per collection.

4.Deciding what to embed and what to reference

MongoDB's official guidance: embed by default, since it answers a query in one read. Reference instead when the data grows independently, is shared across many parents, or is updated far more often than it is read together with its parent.

Key features

  • Default guidance: embed — retrieves related data in a single read, no $lookup
  • Reference when the app frequently updates the related data independently
  • Reference when the related data is shared across many parents, to avoid duplicate updates
  • Reference when the related data can grow without bound relative to its parent
  • The decision is per-relationship, not one global choice for the whole schema

Three questions that decide embed vs. reference

Three questions that decide embed vs. reference
QuestionPoints toward
Is this data almost always read together with its parent?Embed
Is this data updated independently of its parent, often?Reference
Is this data shared across many parent documents?Reference
Could this data grow without a natural bound?Reference
Embed by default, reference on these signals

Embed (default)

read together often

one query, no $lookup

bounded, parent-owned

naturally belongs inside

Reference

shared across parents

avoid duplicated updates

independent lifecycle

updated separately, grows unbounded

  • Embed (default)
    • read together often — one query, no $lookup
    • bounded, parent-owned — naturally belongs inside
  • Reference
    • shared across parents — avoid duplicated updates
    • independent lifecycle — updated separately, grows unbounded

Command

text
// Embed: { parent: { ..., child: { ... } } }
// Reference: { parent: { ..., childId: <id> } }  and a separate child collection

Embed by default — one read, no $lookup. Reference when data is updated independently, shared across many parents, or can grow unbounded.

5.Embedding trade-offs

Embedding gives locality (one read gets everything), atomic single-document updates, and simpler application code — at the cost of documents that can grow, sometimes without a natural bound.

Key features

  • Locality: related data is fetched in a single read, no $lookup required
  • Atomic updates: MongoDB guarantees atomicity within a single document, incl. nested fields
  • Simpler reads: the app receives one shaped object, no client-side assembly
  • Document growth: every embedded item adds to the parent size, up to the 16 MiB limit
  • Growth also raises write cost: a larger document costs more to rewrite for a small update

Embedding: benefit vs. its matching cost

Embedding: benefit vs. its matching cost
BenefitWhat it buysMatching cost
Localityone read returns everythingthe document is exactly as large as everything embedded
Atomic updatesa nested field/array update is atomicno cross-document atomicity is needed, but growth still applies
Simpler readsno application-side join logicschema flexibility can let embedded shapes drift over time
(all three)fast, simple accessa 16 MiB document size ceiling, and growing documents cost more to rewrite
Embedding: what it buys vs. what it costs

Benefits

  • Locality — one read gets everything
  • Atomic single-document updates
  • Simpler reads — no client-side assembly

Cost

  • Every embedded item grows the parent
  • 16 MiB document size ceiling
  • Larger documents cost more to rewrite
  • Benefits
    • Locality — one read gets everything
    • Atomic single-document updates
    • Simpler reads — no client-side assembly
  • Cost
    • Every embedded item grows the parent
    • 16 MiB document size ceiling
    • Larger documents cost more to rewrite

Command

text
{ parent: { ..., embeddedArray: [ {...}, {...} ] } }  // grows as items.push(), one document, one read

Embedding buys locality, atomic single-document updates, and simple reads — paid for as document growth, up to the 16 MiB limit.

6.Referencing trade-offs

Referencing gives independent growth, reuse of shared data across many documents, and smaller documents per record — at the cost of needing an extra query or a $lookup to assemble related data back together.

Key features

  • Independent growth: a referenced collection isn't bounded by any one parent's size
  • Reuse: one referenced document can be pointed to by many others, no duplication
  • Smaller documents: each stores only an id for the relationship, not the data itself
  • Additional queries: reading related data needs a second query or a $lookup stage
  • $lookup is slower than an embedded field read — the cost referencing trades locality for

Referencing: benefit vs. its matching cost

Referencing: benefit vs. its matching cost
BenefitWhat it buysMatching cost
Independent growtha shared entity is never bounded by a parent's sizean extra query to fetch it
Reuseupdate once, every reference reflects itno automatic denormalized copy for fast reads
Smaller documentsthe parent stays lean regardless of how much related data existsassembling a full view needs more round trips
(all three)flexibility and single-source-of-truth updatesa $lookup or app-level join where embedding needed none
Referencing: what it buys vs. what it costs

Benefits

  • Independent growth — not bounded by a parent
  • Reuse — update once, every reference reflects it
  • Smaller documents — just an id

Cost

  • An extra query, or a $lookup, per read
  • No automatic denormalized copy for fast reads
  • More round trips to assemble a full view
  • Benefits
    • Independent growth — not bounded by a parent
    • Reuse — update once, every reference reflects it
    • Smaller documents — just an id
  • Cost
    • An extra query, or a $lookup, per read
    • No automatic denormalized copy for fast reads
    • More round trips to assemble a full view

Command

text
{ parent: { ..., childId: <id> } }   // small, references a separate collection
db.<collection>.aggregate([ { $lookup: { from, localField, foreignField, as } } ])  // join at query time

Referencing buys independent growth, reuse, and small documents — paid for with an extra query or $lookup per read.

7.Bounded vs. unbounded arrays

A bounded array has a natural, small, predictable maximum size (a product's 3-5 variants). An unbounded array has no such ceiling (every comment ever posted, every event ever logged) and keeps growing for the life of the document.

Key features

  • Bounded: a domain rule caps the size (e.g. "a product has at most 6 photos")
  • Unbounded: nothing in the domain caps it — it grows for the life of the parent document
  • An unbounded embedded array risks approaching the 16 MiB document size limit over time
  • An unbounded array also means every read/write of the parent moves an ever-larger payload
  • Bounded arrays are good embedding candidates; unbounded ones signal referencing instead

Bounded vs. unbounded, by example

Bounded vs. unbounded, by example
ExampleBounded?Why
A product's size variantsboundeda product realistically has a handful of sizes
A user's last 5 login eventsboundedcapped by the "last 5" rule itself, e.g. via $slice
All comments on a popular postunboundednothing stops comment count from growing indefinitely
A user's full order historyunboundedgrows for the account's entire lifetime

Command

text
$push: { arrayField: { $each: [<item>], $slice: <-N> } }  // keeps an embedded array bounded to the last N items

Bounded means something actively caps growth (a domain rule, or $slice) — not just "it's small today." Unbounded arrays are a signal to reference.

8.Document growth and the risks of unbounded embedding

Documents have a hard 16 mebibyte size limit. An unbounded embedded array works fine while small, then degrades gradually — larger reads, larger rewrites — before eventually hitting the limit and failing writes.

Key features

  • The maximum BSON document size is 16 mebibytes — a hard limit, not a soft guideline
  • A write that would grow a document past 16 MiB fails outright
  • Before hitting the limit, growth already costs more per read/write as it gets larger
  • Classic failure case: an embedded array with no natural bound (all comments, all events)
  • GridFS is for legitimately large binary content — not the fix for an unbounded array

The two stages of document growth risk

The two stages of document growth risk
StageSymptomFix
Gradual degradationreads/writes slowly get slower as the document growsbound the array, or move it to a referenced collection before it matters
Hard limitwrites fail outright once 16 MiB is reachedthe same fix, now urgent — the document may also need to be split or migrated
Growth risk, two stages

small document

fast reads/writes

growing document

gradually slower

16 MiB limit

writes start failing

  1. small document — fast reads/writes
  2. growing document — gradually slower
  3. 16 MiB limit — writes start failing

Command

text
// No syntax to show — the risk is structural: an unbounded $push target inside one document.

16 MiB is a hard document limit, not a guideline. Unbounded arrays degrade gradually before failing outright — design them out at schema time.

MongoDB's core data-modeling principle in one sentence: data that is accessed together should be stored together. This is the underlying reason embedding is the default recommendation, not a separate rule to memorize on top of it.

Key features

  • Core principle: "data that's accessed together should be stored together"
  • The reasoning behind "embed by default" — a direct application of the principle
  • Co-location is about access, not conceptual relatedness — unrelated fields can benefit too
  • Limits: growth, sharing, and independent-update needs are reasons not to co-locate
  • Applying it needs the access-pattern list — you can't co-locate what isn't identified

The principle in action, across this section's other concepts

The principle in action, across this section's other concepts
ConceptHow it applies the principle
Access patternsidentifies what is actually accessed together, the input the principle needs
Embed vs. referenceembedding IS co-location; referencing is the exception when co-location is not safe
Bounded arraysco-location is safe when the co-located data has a natural size limit
Document growth riskthe failure mode when co-location is applied without a bound
One principle, applied across the section

Confirms it

Access patterns

identifies what is read together

Bounded arrays

co-location is safe with a size limit

Overrides it

Referencing

when co-location is not safe

Document growth risk

the failure mode without a bound

  • Data accessed together should be stored together
  • Confirms it
    • Access patterns — identifies what is read together
    • Bounded arrays — co-location is safe with a size limit
  • Overrides it
    • Referencing — when co-location is not safe
    • Document growth risk — the failure mode without a bound

Command

text
// The principle, not a syntax: "data accessed together" → "data stored together" → one document, one read.

"Data accessed together should be stored together" — the reason embedding is the default. Applies by access pattern, not just entity relationship.

Remember

  • Design from access patterns and workload types, not from an entity-relationship diagram alone.
  • Embed by default for locality, atomic updates, and simple reads; reference for growth, sharing, or independent updates.
  • A bounded array has an enforced cap (a rule, or $slice) — current small size is not the same claim.
  • The 16 MiB document limit is hard — unbounded embedding degrades gradually before failing writes outright.

Pro tip

  • "Accessed together" applies by access pattern, not just by conceptual entity — co-locate unrelated-seeming fields read together on the same page.
  • Classify each access pattern's workload type (read/write-heavy, time-series, transactional, ...) — known types point to known-good patterns.

Next up

  • Embedding vs. Referencing
  • Schema Design Patterns
  • Schema Validation

Sources

  • Data modeling — mongodb.com/docs/manual/data-modeling/
  • Map schema relationships — mongodb.com/docs/manual/data-modeling/schema-design-process/map-relationships/
  • BSON document size limit — mongodb.com/docs/manual/reference/limits/
  • Checked 2026-08-21

Spotted a mistake on this page? Tell us.