6.2 Core Schema Design Patterns

Key Takeaways

  • The Polymorphic Pattern stores documents with different shapes in the same collection using a discriminator field (e.g., 'type') for unified querying.
  • The Attribute Pattern transforms sparse attributes into key-value arrays ('[{ k: "color", v: "red" }]'), allowing 1 compound multikey index to cover hundreds of dynamic specs without hitting the 64-index limit.
  • The Bucket Pattern groups streaming time-series data into time-bounded documents with pre-aggregated summaries, shrinking document count and index size.
  • The Subset Pattern satisfies the 80/20 rule by embedding frequently accessed items in the main document while offloading complete history to a referenced collection.
  • The Extended Reference Pattern copies read-critical fields to eliminate '$lookup' joins, while the Computed Pattern pre-calculates rollups on write to avoid on-the-fly aggregation.
Last updated: September 2026

6.2 Core Schema Design Patterns

Designing scalable schemas in MongoDB requires moving beyond naive normalization or indiscriminate embedding. Real-world applications encounter specific data access challenges: sparse product catalogs, high-frequency IoT telemetry, unbounded comments, and heavy read-to-write ratios.

To address these recurring engineering challenges, MongoDB architects established six Core Schema Design Patterns:

  1. Polymorphic Pattern
  2. Attribute Pattern
  3. Bucket Pattern
  4. Subset Pattern
  5. Extended Reference Pattern
  6. Computed Pattern

Mastering these patterns enables developers to optimize the WiredTiger storage engine, reduce B-tree index footprints, keep the working set residing in RAM, and eliminate expensive $lookup aggregation stages.


1. The Polymorphic Pattern

The Problem

In object-oriented software engineering, classes inherit from common abstract bases (e.g., Vehicle -> Car, Truck, Motorcycle or Content -> Article, Video, Podcast). In relational databases, modeling inheritance requires Table-Per-Hierarchy (sparse columns with many nulls) or Table-Per-Type (multiple tables joined on primary keys), both of which are cumbersome and slow.

The Solution

The Polymorphic Pattern stores documents with different structural shapes inside the same collection. A designated discriminator field (commonly named type, kind, category, or schema_version) identifies the document sub-type. Shared attributes reside at the root level, while type-specific attributes are encapsulated in specialized fields or embedded subdocuments.

// Single 'content' collection storing polymorphic documents

// 1. Article Document
db.content.insertOne({
  _id: ObjectId("66d5b100f1e8a93b4c5d6e31"),
  type: "ARTICLE", // Discriminator
  title: "Introduction to MongoDB Indexing",
  author_id: ObjectId("66d5b100f1e8a93b4c5d6e01"),
  published_at: ISODate("2026-09-01T08:00:00Z"),
  word_count: 1450,
  body: "Indexing in MongoDB is powered by B-trees..."
});

// 2. Video Document
db.content.insertOne({
  _id: ObjectId("66d5b100f1e8a93b4c5d6e32"),
  type: "VIDEO", // Discriminator
  title: "Aggregation Pipeline Masterclass",
  author_id: ObjectId("66d5b100f1e8a93b4c5d6e02"),
  published_at: ISODate("2026-09-02T10:30:00Z"),
  duration_seconds: 3600,
  video_url: "https://cdn.example.com/videos/agg-pipeline.mp4",
  resolutions: ["1080p", "4K"]
});

Querying and Indexing

Applications can query across all content types in a single query or filter by specific types using the discriminator field:

// Find all published content by an author regardless of format
db.content.find({ author_id: ObjectId("66d5b100f1e8a93b4c5d6e01") }).sort({ published_at: -1 });

// Compound index supporting discriminator queries
db.content.createIndex({ type: 1, published_at: -1 });

2. The Attribute Pattern

The Problem

E-commerce catalogs and product inventories often contain thousands of distinct product categories. Each category has unique, sparse specifications:

  • Televisions: screen_size, refresh_rate, panel_type, hdmi_ports
  • Shoes: size, color, material, gender
  • Tires: diameter, aspect_ratio, load_index, speed_rating

If modeled as top-level fields ({ screen_size: 65, refresh_rate: 120 }), supporting user searches across these attributes would require creating dozens or hundreds of individual indexes. MongoDB imposes a hard limit of 64 indexes per collection, and each index consumes substantial RAM and degrades write performance.

The Solution

The Attribute Pattern transforms sparse, heterogeneous fields into an array of key-value subdocuments (commonly k and v):

Before (Sparse Fields):  { _id: 1, title: "4K TV", screen_size: 65, refresh_rate: 120 }
                                   |
                                   v (Attribute Pattern)
After (Key-Value Array): { _id: 1, title: "4K TV", specs: [
                             { k: "screen_size", v: 65, u: "inches" },
                             { k: "refresh_rate", v: 120, u: "Hz" }
                           ]}
// Product document modeled with Attribute Pattern
db.products.insertOne({
  _id: ObjectId("66d5b200f1e8a93b4c5d6e40"),
  sku: "PROD-TV-4K-01",
  title: "Ultra HD Smart TV 65 Inch",
  category: "Electronics",
  price: NumberDecimal("799.99"),
  specs: [
    { k: "screen_size", v: 65, u: "inches" },
    { k: "refresh_rate", v: 120, u: "Hz" },
    { k: "resolution", v: "3840x2160" },
    { k: "hdr_support", v: true }
  ]
});

Indexing Strategy

A single compound multi-key index covers all search queries across every dynamic attribute:

// One index covers search on any attribute key-value combination!
db.products.createIndex({ "specs.k": 1, "specs.v": 1 });

// Query: Find TVs with screen_size >= 65
db.products.find({
  specs: { $elemMatch: { k: "screen_size", v: { $gte: 65 } } }
});

Exam Tip: Whenever a scenario describes "sparse fields across diverse product categories", "avoiding the 64 index limit", or "searching arbitrary dynamic properties with a single index", the correct pattern is always the Attribute Pattern.

3. The Bucket Pattern

The Problem

In time-series, financial tick feeds, and Internet of Things (IoT) workloads, devices transmit measurements at high frequencies (e.g., 1 reading every second). Storing each measurement as a standalone document creates billions of tiny documents. This leads to massive B-tree index bloat, heavy document metadata overhead (the 16-byte BSON header per document), and millions of random disk I/O operations.

The Solution

The Bucket Pattern groups streaming data points into a single bucket document bounded by a time window (e.g., 1 hour, 1 day) or a maximum sample count (e.g., 200 readings). The bucket document stores common metadata, pre-computed summary metrics (min, max, sum, count), and an array of individual measurements.

// Bucket Document: 1 hour of temperature readings from sensor_east_01
db.sensor_buckets.insertOne({
  _id: ObjectId("66d5b300f1e8a93b4c5d6e50"),
  sensor_id: "SENSOR-EAST-01",
  bucket_start: ISODate("2026-09-02T10:00:00Z"),
  bucket_end: ISODate("2026-09-02T10:59:59Z"),
  count: 3600,
  summary: {
    min_temp: 21.4,
    max_temp: 24.8,
    sum_temp: 83160.0,
    avg_temp: 23.1
  },
  readings: [
    { timestamp: ISODate("2026-09-02T10:00:01Z"), temp: 21.4, humidity: 45 },
    { timestamp: ISODate("2026-09-02T10:00:02Z"), temp: 21.5, humidity: 45 },
    // ... 3598 additional readings
  ]
});

Atomic Streaming Ingestion with $inc and $push

// Upserting incoming data into current hour's bucket
db.sensor_buckets.updateOne(
  {
    sensor_id: "SENSOR-EAST-01",
    bucket_start: ISODate("2026-09-02T10:00:00Z"),
    count: { $lt: 3600 } // Bucket capacity limit
  },
  {
    $push: { readings: { timestamp: new Date(), temp: 22.8, humidity: 46 } },
    $inc: { count: 1, "summary.sum_temp": 22.8 },
    $min: { "summary.min_temp": 22.8 },
    $max: { "summary.max_temp": 22.8 },
    $setOnInsert: { bucket_start: ISODate("2026-09-02T10:00:00Z") }
  },
  { upsert: true }
);

Benefits of the Bucket Pattern

  • Reduces document count by orders of magnitude (e.g., 3,600:1 ratio).
  • Shrinks index size so indexes remain entirely within RAM.
  • Summary metrics provide instant analytical rollups without running aggregation pipelines over raw samples.

4. The Subset Pattern

The Problem

Many 1:N relationships experience the 80/20 Rule: 80% of application queries access only a small, recent, or top-ranked subset of the related data, while the full history is rarely viewed. Examples include:

  • E-commerce products with 5,000 customer reviews (users only view the top 10 recent reviews on the product detail page).
  • Movies with 200 cast members (users only view the top 5 lead actors on the main screen).

Embedding all 5,000 reviews inflates document size to several megabytes, wastes RAM, and degrades network throughput. Conversely, separating all reviews into another collection requires a $lookup join on every single product page load.

The Solution

The Subset Pattern splits the 1:N relationship into two components:

  1. Embedded Subset: Embed the top N most frequently accessed items (e.g., top 10 most recent reviews) directly inside the main document.
  2. Referenced Full Collection: Store the complete historical list in a separate collection using Parent Referencing.
// 1. Primary Product Document (Embeds Top 5 Reviews for Fast Page Rendering)
db.products.insertOne({
  _id: ObjectId("66d5b400f1e8a93b4c5d6e60"),
  title: "Wireless Noise-Canceling Headphones",
  price: NumberDecimal("249.99"),
  rating_average: 4.8,
  review_count: 3420,
  recent_reviews: [ // TOP SUBSET (Fixed to 5 items)
    {
      reviewer: "Alex K.",
      rating: 5,
      comment: "Exceptional sound clarity and battery life.",
      date: ISODate("2026-09-01T14:00:00Z")
    },
    {
      reviewer: "Maria G.",
      rating: 5,
      comment: "Best ANC for frequent travelers.",
      date: ISODate("2026-08-30T09:15:00Z")
    }
  ]
});

// 2. Full 'reviews' collection (Stores all 3,420 historical reviews)
db.reviews.insertOne({
  _id: ObjectId("66d5b400f1e8a93b4c5d6e61"),
  product_id: ObjectId("66d5b400f1e8a93b4c5d6e60"), // Parent Reference
  reviewer: "Alex K.",
  rating: 5,
  comment: "Exceptional sound clarity and battery life.",
  date: ISODate("2026-09-01T14:00:00Z")
});

When a user opens a product page, MongoDB fetches the product and its top reviews in a single fast read. If the user clicks "View all 3,420 reviews", the application queries the reviews collection with pagination (skip/limit).


5. The Extended Reference Pattern

The Problem

In normalized referencing, querying an operational document (e.g., an Order or Invoice) requires executing a $lookup stage to pull basic information (such as Customer Name or Supplier City) from the referenced document. In high-throughput transaction systems, joining collections on every read introduces significant CPU and memory overhead.

The Solution

The Extended Reference Pattern copies a small, frequently accessed subset of fields from the referenced document directly into the operational document at create time.

// Customer Document (Full master record)
db.customers.insertOne({
  _id: ObjectId("66d5b500f1e8a93b4c5d6e70"),
  name: "Precision Engineering Ltd",
  tax_id: "US-9481029",
  phone: "+1-408-555-0199",
  billing_address: { street: "500 Main St", city: "San Jose", state: "CA", zip: "95112" },
  credit_rating: "AAA",
  internal_notes: "Key enterprise account managed by Sarah."
});

// Order Document (Uses Extended Reference Pattern)
db.orders.insertOne({
  _id: ObjectId("66d5b500f1e8a93b4c5d6e71"),
  order_number: "PO-88412",
  order_date: ISODate("2026-09-02T11:00:00Z"),
  status: "SHIPPED",
  total_amount: NumberDecimal("5420.00"),
  customer: { // EXTENDED REFERENCE (Only frequently needed fields)
    _id: ObjectId("66d5b500f1e8a93b4c5d6e70"),
    name: "Precision Engineering Ltd",
    shipping_city: "San Jose",
    phone: "+1-408-555-0199"
  }
});

Point-in-Time Historical Accuracy

The Extended Reference Pattern also provides point-in-time snapshots. If a customer changes their corporate name or billing address next year, past orders and invoices must preserve the exact name and address that was valid at the time the order was placed. Denormalizing the extended reference creates a permanent, immutable record.


6. The Computed Pattern

The Problem

Applications frequently display summary calculations: total revenue for a merchant, total movie ticket sales, average student GPA, or total unread notifications. Running on-the-fly aggregation queries ($group, $avg, $sum) across millions of records on every user read request exhausts database CPU and increases read latencies.

The Solution

The Computed Pattern calculates rollup values incrementally during write operations (or via asynchronous background workers) and stores the persisted aggregate directly in the parent document.

// Movie document with pre-calculated computed aggregates
db.movies.insertOne({
  _id: ObjectId("66d5b600f1e8a93b4c5d6e80"),
  title: "Interstellar Odyssey",
  release_year: 2026,
  total_votes: 1250,        // Computed Count
  sum_stars: 6000,          // Computed Running Sum
  average_rating: 4.80      // Computed Value: (sum_stars / total_votes)
});

// When a new review with 5 stars is submitted, update computed fields atomically:
db.movies.updateOne(
  { _id: ObjectId("66d5b600f1e8a93b4c5d6e80") },
  [
    {
      $set: {
        total_votes: { $add: [ "$total_votes", 1 ] },
        sum_stars: { $add: [ "$sum_stars", 5 ] },
        average_rating: {
          $divide: [
            { $add: [ "$sum_stars", 5 ] },
            { $add: [ "$total_votes", 1 ] }
          ]
        }
      }
    }
  ]
);

7. Master Schema Design Patterns Summary Table

PatternCore Problem AddressedWhen to ApplyKey Structural TransformationPrimary Benefit
PolymorphicDocuments have different fields but share common traitsInheritance hierarchies, CMS content, event logsUse a discriminator field (type); store in single collectionSingle collection for related types; unified queries
AttributeHundreds of sparse, dynamic product attributesProduct catalogs with varying specs, hitting 64-index limitTransform sparse fields to key-value array [{ k: ..., v: ... }]1 compound multi-key index covers all attributes
BucketHigh-frequency streaming telemetry causing index bloatIoT data, financial tick data, time-series metricsGroup samples into time/count-bounded bucket documentsReduces document count 3600:1; compact indexes in RAM
Subset1:N relationship with 80/20 access skew (large histories)Product reviews, movie cast, social media commentsEmbed top N items in main doc; store full list in separate collectionFast primary reads; working set stays small in RAM
Extended ReferenceHigh-frequency $lookup joins on read pathsE-commerce orders, invoices, shipment trackingEmbed 2–4 frequently read fields from referenced doc into main docEliminates $lookup joins; creates point-in-time snapshot
ComputedExpensive on-the-fly aggregations executed repeatedlyTotals, average ratings, running counts, financial balancesPre-calculate aggregate during write and store scalar in docRead operations cost O(1) time with zero aggregation CPU
Loading diagram...
MongoDB Core Schema Design Patterns Architecture
Test Your Knowledge

An online retail platform sells products across 50 categories. Products have distinct technical specifications (e.g., screen size for TVs, thread count for sheets, horsepower for lawn mowers). Customers must be able to filter by any specification. The engineering team is approaching MongoDB's 64-index limit per collection. Which schema design pattern resolves this issue?

A
B
C
D
Test Your Knowledge

A movie streaming service displays a film detail page visited millions of times daily. The page shows basic film information and the 5 most recent reviews. Films can have up to 20,000 total reviews, but users rarely click 'View All Reviews'. Which pattern should the architect implement to maximize cache performance and prevent large document sizes?

A
B
C
D
Test Your Knowledge

An order management system processes 50,000 orders per hour. On every order status lookup, the application requires the customer's full name, phone number, and delivery city. Querying the customer collection with $lookup on every order read is causing excessive CPU load. Which design pattern eliminates this join overhead?

A
B
C
D
Test Your Knowledge

A weather monitoring system captures temperature and air pressure readings every second from thousands of weather stations. Storing each reading in a distinct document is causing severe index memory bloat. Which pattern groups these readings into time-bounded documents with pre-aggregated summary statistics?

A
B
C
D