Module: MongoDB
MongoDB·135·6 MIN READ

135: MongoDB Data Modeling — Access Patterns, Embedding, Referencing, Schema Validation, and Anti-Patterns

TOPICS COVERED: MongoDB Data Modeling — Access Patterns, Embedding, Referencing, Schema Validation, and Anti-Patterns

Learning objectives

You will learn to:

  • design MongoDB schemas from access patterns;
  • choose embedding versus referencing;
  • model one-to-one, one-to-few, one-to-many, and many-to-many relationships;
  • understand document growth and bounded arrays;
  • apply Extended Reference, Subset, Bucket, and Computed-style patterns conceptually;
  • understand polymorphic documents;
  • use schema validation;
  • plan schema evolution;
  • avoid joins/transactions caused by poor document boundaries;
  • understand multi-tenant modeling;
  • reason about denormalization and consistency.

Start from access patterns

Do not ask:

What are my entities?

first.

Ask:

What does the application read and write together?

For an order page, common read:

text
order header
customer snapshot
line items
totals
shipping address

Embedding those can make one read return the order aggregate.

Relational instinct versus document model

Relational normalized:

text
orders
order_items
addresses
customers

Mongo can embed:

javascript
{
  _id: ObjectId(...),
  customer: {
    id: ObjectId(...),
    name: "Maya",
    email: "..."
  },
  items: [
    {
      productId: ObjectId(...),
      name: "Notebook",
      quantity: 2,
      pricePaise: 12000
    }
  ],
  shippingAddress: {...},
  totals: {...}
}

This intentionally duplicates customer/product snapshot data because historical order should preserve what was purchased.

Denormalization can be correct.

Embedding advantages

  • one read;
  • atomic update within document;
  • natural aggregate;
  • fewer joins;
  • locality.

Embedding disadvantages

  • duplication;
  • document growth;
  • large updates;
  • repeated data synchronization if canonical field must change everywhere;
  • document size limits.

Referencing advantages

  • independent lifecycle;
  • avoids huge duplication;
  • avoids unbounded arrays;
  • many relationships.

Disadvantages:

  • additional query;
  • $lookup;
  • application joins;
  • multi-document consistency.

One-to-one

If data always read together and same lifecycle:

javascript
{
  profile: {
    displayName: "...",
    timezone: "Asia/Tokyo"
  }
}

Embed.

If independently secured/huge/lifecycle different, separate collection may fit.

One-to-few

Order line items usually bounded.

Embed:

javascript
items: [...]

One-to-many

Blog author with millions of posts.

Do not:

javascript
{
  userId,
  postIds: [
    // millions forever
  ]
}

Instead posts reference author:

javascript
{
  _id,
  authorId,
  ...
}

Query posts by indexed authorId.

Many-to-many

Students ↔ courses.

Options:

  • references in one side;
  • references both sides;
  • enrollment collection.

If relationship itself has data:

javascript
{
  studentId,
  courseId,
  enrolledAt,
  status,
  grade
}

an enrollment collection is natural.

Boundedness test

Before embedding array, ask:

text
maximum expected count?
upper hard bound?
can it grow forever?
how often updated?
how often entire array read?

If no meaningful bound, avoid unbounded embedding.

Document growth

Repeatedly pushing into huge document can cause:

  • larger reads/writes;
  • index expansion;
  • document size limit;
  • contention on one document.

Time-series/event-style data often needs bucket/separate documents.

Duplication is a trade-off

Product name duplicated in orders.

Should historical order name update if product renamed?

Often no.

Then duplicate snapshot is domain-correct.

Customer current phone duplicated in account dashboard where should always be current?

Maybe reference/query canonical customer instead.

Ask whether duplicated value is:

text
snapshot

or:

text
cached canonical field

The second needs synchronization strategy.

Extended Reference pattern

Keep reference plus frequently needed small fields:

javascript
{
  customer: {
    id: ObjectId(...),
    displayName: "Maya"
  }
}

Avoid extra lookup for display.

Canonical customer record remains source for mutable profile.

If displayName changes, decide whether old order should change.

Subset pattern

A main document may embed only frequently needed subset of related data.

Example product:

javascript
{
  recentReviews: [
    // last 3
  ]
}

Full reviews separate collection.

This improves common read but requires update logic.

Computed pattern

Store precomputed value:

javascript
{
  reviewCount: 238,
  averageRating: Decimal128("4.6")
}

instead of aggregating all reviews every request.

Now writes must maintain computed values or async rebuild.

Trade read performance for write complexity.

Bucket pattern

Group many small records into bounded buckets.

Example sensor/events by hour/day.

javascript
{
  deviceId,
  bucketStart,
  measurements: [...]
}

Useful when individual event documents would be enormous in count and read in groups.

MongoDB time-series collections provide specialized behavior for time-series use cases; prefer them where appropriate.

Polymorphic collections

Example notifications:

javascript
{
  type: "email",
  to: "...",
  subject: "..."
}
javascript
{
  type: "sms",
  to: "...",
  message: "..."
}

Shared common fields can coexist.

Use discriminator/type field and validator rules.

Avoid collection becoming unrelated junk drawer.

Schema validation

Use $jsonSchema.

Example:

javascript
db.createCollection("orders", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: [
        "tenantId",
        "status",
        "items",
        "createdAt"
      ],
      properties: {
        tenantId: {
          bsonType: "objectId"
        },
        status: {
          enum: [
            "draft",
            "placed",
            "completed",
            "cancelled"
          ]
        },
        items: {
          bsonType: "array",
          minItems: 1
        },
        createdAt: {
          bsonType: "date"
        }
      }
    }
  }
})

Database validation protects all writers.

Application schema can give richer user errors.

Validation levels/actions

MongoDB validation settings can control how validation applies and whether invalid writes error/warn.

Use strict production policy unless migration scenario intentionally needs staged rollout.

Schema migration

MongoDB flexible schema means migrations still exist.

Example v1:

javascript
{
  name: "Maya"
}

v2:

javascript
{
  profile: {
    name: "Maya"
  },
  schemaVersion: 2
}

Strategies:

Eager migration

Script updates all documents.

Lazy read migration

Application reads old shape and upgrades on write/read.

Dual-read/write transition

Temporarily support both.

Do not leave indefinite ambiguous schema if code becomes impossible to reason about.

Schema version field

javascript
{
  schemaVersion: 3
}

Useful for complex long-lived polymorphic migrations.

Not required for every simple collection.

Multi-tenant modeling

Common shared collection:

javascript
{
  tenantId: ObjectId(...),
  ...
}

Every query includes tenant.

Indexes usually start with tenant where query patterns require:

javascript
{ tenantId: 1, createdAt: -1 }

Do not trust tenantId from client body.

Derive server auth context.

Database per tenant

Alternative for strong isolation/small tenant count.

Trade-offs:

  • many databases;
  • migrations;
  • connections;
  • operations.

Collection per tenant is often operationally painful.

Choose tenant strategy intentionally.

Hot documents

One document updated extremely frequently becomes contention hotspot.

Example:

javascript
{
  globalCounter: ...
}

at huge write rate.

Consider sharded counters/bucketing/event design.

MongoDB can handle atomic updates, but one document has finite write throughput.

Arrays and indexes

Multikey indexes index array elements.

A document with huge array can produce many index entries.

Compound multikey rules/limitations matter.

Do not create giant arrays and expect indexes to make them free.

$lookup as design signal

MongoDB supports join-like $lookup.

It is useful.

But if every primary query performs many complex lookups across normalized collections, revisit whether MongoDB document model is being used effectively.

Do not ban $lookup; treat frequent expensive joins as modeling signal.

Transactions as design signal

Multi-document transactions are valid.

But if basic aggregate update always requires 8 documents, perhaps document boundary is too normalized.

Again, not a rule—just a design question.

Data duplication consistency

Strategies:

Synchronous

Update canonical and duplicates in transaction.

Eventual

Change canonical; background worker updates projections.

Snapshot

Do not update historical duplicate.

Document which semantics apply.

Deletion

If referenced document deleted, MongoDB does not automatically enforce foreign-key cascade.

Application must define:

  • restrict delete;
  • soft delete;
  • cascade;
  • orphan allowed;
  • cleanup job.

Do not assume referential integrity exists automatically.

Soft delete

javascript
{
  deletedAt: ISODate(...)
}

Benefits:

  • recovery/audit.

Costs:

  • every query must exclude;
  • unique indexes need partial strategy;
  • data grows;
  • accidental leakage.

Use if domain needs it, not by default.

Audit history

Do not grow:

javascript
history: [...]

forever inside record.

Use audit collection/event store.

Access-pattern worksheet

For each collection:

text
Top reads:
Top writes:
Filter fields:
Sort fields:
Expected document size:
Expected array bounds:
Atomic update boundary:
Deletion behavior:
Tenant scope:
Retention:
Indexes:

Design index/schema together.

Anti-patterns

  • one collection per tenant/user;
  • huge unbounded arrays;
  • arbitrary key/value mega-document;
  • relational schema copied 1:1 without reason;
  • embed canonical mutable object everywhere with no sync plan;
  • reference everything because SQL did;
  • transaction for every basic write;
  • no tenant field/index plan;
  • no deletion/schema evolution strategy.

Exercises

  1. Model order aggregate.
  2. Model user + millions of posts.
  3. Model student-course enrollment.
  4. Choose snapshot versus canonical duplication.
  5. Design bounded recent reviews subset.
  6. Design audit history collection.
  7. Add tenant scoping.
  8. Write collection validator.
  9. Plan v1→v2 schema migration.
  10. Review a relational schema and intentionally remodel for Mongo access patterns.

Mastery checklist

Explain:

  • access-pattern-first modeling;
  • embed/reference;
  • bounded arrays;
  • denormalization;
  • subset/computed/bucket patterns;
  • polymorphism;
  • validation;
  • migration;
  • multi-tenancy;
  • $lookup/transaction design signals;
  • deletion and soft-delete trade-offs.

Official references