Study guide
6.2 Core Schema Design Patterns
MongoDB Associate Developer Exam
Track your progress as you read
Finished sections get a check mark.
1 Chapter 1: Document Model & MongoDB Architecture
1.1 Document Model Fundamentals & BSON Data Types 1.2 Databases, Collections & Schema Flexibility 1.3 Distributed Architecture Fundamentals: Replica Sets & Sharded Clusters
2 Chapter 2: CRUD Operations: Create & Read
2.1 Document Insertion Operations 2.2 Query Selectors & Filter Operators 2.3 Array Queries & Element Evaluation 2.4 Projections, Cursor Methods & Collation
3 Chapter 3: CRUD Operations: Update & Delete
3.1 Field Update Operators 3.2 Array Update Operators & Positional Modifiers 3.3 Document Replacement & Deletion Operations 3.4 Atomic Find-and-Modify Operations
4 Chapter 4: Aggregation Framework Mastery
4.1 Pipeline Fundamentals & Core Stages ($match, $project, $addFields) 4.2 Grouping, Reshaping & Array Manipulation ($group, $unwind, $replaceWith) 4.3 Multi-Document Joins & Faceted Pipelines ($lookup, $facet, $bucket) 4.4 Materialization & Window Operations ($out, $merge, $setWindowFields) 4.5 Atlas Search: Search Index Definitions & $search Queries
5 Chapter 5: Indexing Strategies & Performance Optimization
5.1 Index Types & Storage Mechanics 5.2 Compound Indexes & the Equality-Sort-Range (ESR) Rule 5.3 Multikey Indexes & Array Indexing Constraints 5.4 Specialized Index Types: TTL, Partial, Unique & Text 5.5 Query Optimization & Explain Plan Analysis
6 Chapter 6: Data Modeling & Schema Design Patterns
6.1 Embedding vs. Referencing Tradeoffs 6.2 Core Schema Design Patterns 6.3 Schema Validation with JSON Schema
7 Chapter 7: Application Development & Driver Integration
7.1 Connection Strings & Connection Pool Management 7.2 Driver CRUD Paradigms & Object Mapping 7.3 Cursor Batching, Streaming & Memory Handling 7.4 Write Concerns, Read Concerns & Retryable Operations
8 Chapter 8: Developer Tooling & Administration Essentials
8.1 mongosh Scripting & Administrative Commands 8.2 Data Ingestion & Export Utilities 8.3 Backup, Restore & Performance Telemetry 8.4 Atlas Sample Dataset & Data Explorer
6.2 Core Schema Design Patterns
Key Takeaways Explain This Topic AI
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 :
- Polymorphic Pattern
- Attribute Pattern
- Bucket Pattern
- Subset Pattern
- Extended Reference Pattern
- 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:
Embedded Subset : Embed the top N most frequently accessed items (e.g., top 10 most recent reviews) directly inside the main document.
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
Pattern Core Problem Addressed When to Apply Key Structural Transformation Primary Benefit
Polymorphic Documents have different fields but share common traits Inheritance hierarchies, CMS content, event logs Use a discriminator field ( type ); store in single collection Single collection for related types; unified queries
Attribute Hundreds of sparse, dynamic product attributes Product catalogs with varying specs, hitting 64-index limit Transform sparse fields to key-value array [{ k: ..., v: ... }] 1 compound multi-key index covers all attributes
Bucket High-frequency streaming telemetry causing index bloat IoT data, financial tick data, time-series metrics Group samples into time/count-bounded bucket documents Reduces document count 3600:1; compact indexes in RAM
Subset 1:N relationship with 80/20 access skew (large histories) Product reviews, movie cast, social media comments Embed top N items in main doc; store full list in separate collection Fast primary reads; working set stays small in RAM
Extended Reference High-frequency $lookup joins on read paths E-commerce orders, invoices, shipment tracking Embed 2–4 frequently read fields from referenced doc into main doc Eliminates $lookup joins; creates point-in-time snapshot
Computed Expensive on-the-fly aggregations executed repeatedly Totals, average ratings, running counts, financial balances Pre-calculate aggregate during write and store scalar in doc Read operations cost O(1) time with zero aggregation CPU