Capstone — Designing a Real Application's Data Model

MongoDB Intermediate/Advanced — Capstone: Designing a Real Application's Data Model
MongoDB Intermediate/Advanced
Chapter 8 · Capstone: Designing a Real Application's Data Model

🏁 Capstone: Designing a Real Application's Data Model

Every chapter in this course has reused the same running example — customers, orders, products. This capstone builds a small, real order-processing system around them, combining schema design (Ch.3), indexing (Ch.7 + mongodb1-7), the aggregation framework (Ch.1–2), transactions (Ch.4), and driver code (mongodb1-8) into one coherent application, end to end.

The Application: A Small Order-Processing System

Requirements: customers place orders for products; each order must record the price paid at that moment (not today's price, which may have changed since); placing an order must safely decrement stock and never double-charge or double-decrement; and the business needs a dashboard showing revenue by category and top-selling products.

Step 1: Schema Design (Chapter 3)

Applying Chapter 3's patterns deliberately, not by default:

// An order EMBEDS a snapshot of what was bought — price/name at time of purchase, // not a live reference, since a later price change must never alter a past order { _id: ObjectId("..."), customer_id: "C104", status: "completed", items: [ { product_id: "P200", name: "Wireless Mouse", category: "electronics", price_paid: 24.99, qty: 1 } ], total: 24.99, createdAt: ISODate("2026-07-06") }

The computed pattern also applies: each customer document carries a precomputed lifetime_spend, updated incrementally on every order rather than re-aggregated on every profile view — exactly Chapter 3's page-view-counter idea, applied to a running total.

Step 2: Indexes (Chapter 7 / mongodb1-7)

The queries this app actually runs decide the indexes it needs — not guesswork:

db.orders.createIndex({ customer_id: 1, createdAt: -1 }) // "this customer's orders, newest first" db.orders.createIndex({ status: 1 }) // the $match in the dashboard pipeline below

Step 3: The Transaction — Placing an Order (Chapter 4)

Placing an order touches three separate documents — a new order, the product's stock, and the customer's precomputed lifetime_spend — which is exactly the multi-document signal Chapter 4 flagged as needing a transaction, not three independent writes.

const session = client.startSession(); try { await session.withTransaction(async () => { await orders.insertOne(newOrder, { session }); await products.updateOne( { _id: "P200" }, { $inc: { stock: -1 } }, { session } ); await customers.updateOne( { _id: "C104" }, { $inc: { lifetime_spend: newOrder.total } }, { session } ); }); } finally { session.endSession(); }

If the stock decrement fails (say, application logic rejects it because stock is already 0), the whole transaction aborts — the order is never left "placed" against a customer whose spend total was never actually updated.

Step 4: The Aggregation — A Sales Dashboard (Chapters 1 & 2)

The dashboard needs two different views in one call — a job for $facet — starting from a $match that can use the status index created in Step 2, per this course's own indexing-order gotcha (Chapter 1 and revisited in Chapter 7):

db.orders.aggregate([ { $match: { status: "completed" } }, { $unwind: "$items" }, { $facet: { revenueByCategory: [ { $group: { _id: "$items.category", revenue: { $sum: "$items.price_paid" } } }, { $sort: { revenue: -1 } } ], topProducts: [ { $group: { _id: "$items.name", unitsSold: { $sum: "$items.qty" } } }, { $sort: { unitsSold: -1 } }, { $limit: 5 } ] } } ])

This chains together every stage this course covered: $match (Ch.1) using an index, $unwind (Ch.2) to break each order's embedded items array into individual rows, and $facet (Ch.2) running two independent grouped-and-sorted views from that same unwound input in a single round trip.

Step 5: Scaling Considerations (Chapters 5 & 6)

Not needed on day one, but worth designing with in mind: the dashboard aggregation is a read-heavy reporting query, a good candidate for secondaryPreferred read preference (Chapter 5) once the replica set is under real load. If orders ever outgrows a single replica set, customer_id — already indexed, already the natural per-customer query boundary — is the obvious shard key candidate (Chapter 6), hashed to avoid the write hotspot a raw, sequential _id or timestamp key would create.

Course ConceptWhere It's Used in This App
Embedding vs. referencing (Ch.3)Order items embed a price/name snapshot; customers are referenced by ID
Computed pattern (Ch.3)customers.lifetime_spend, updated via $inc on every order
Compound indexes (Ch.7){ customer_id, createdAt } and { status }, matching real queries
Transactions (Ch.4)Placing an order: insert + stock decrement + spend increment, atomically
$match / $unwind / $facet (Ch.1–2)The dashboard's revenue-by-category and top-products aggregation
Read preference (Ch.5)Dashboard reads candidate for secondaryPreferred at scale
Shard key choice (Ch.6)Hashed customer_id, if/when orders needs sharding

💻 Coding Challenges

Challenge 1: Add a Cancellation Path

Design the transaction for cancelling an order: it must set the order's status to "cancelled", restore the product's stock, and decrement the customer's lifetime_spend — all atomically.

Goal: Practice recognizing that a cancellation is the same multi-document-atomicity problem as placing an order, just reversed.

→ Solution

Challenge 2: Extend the Dashboard Pipeline

Add a third facet to Step 4's aggregation: ordersPerDay, counting completed orders grouped by calendar day.

Goal: Practice adding a third independent sub-pipeline to an existing $facet stage without disturbing the other two.

→ Solution

Challenge 3: Justify the Embedding Choice

A colleague suggests referencing the product instead of embedding a name/price_paid snapshot in each order item, to "keep it DRY." Explain, using this course's material, why that would actually introduce a bug.

Goal: Practice defending a specific schema design decision against a plausible-sounding but incorrect simplification.

→ Solution

⚠️ Gotcha: Pieces That Work Alone Don't Automatically Work Together

Each step above was verified against this course's own earlier examples — but combining them is still worth testing end to end, not assumed to compose correctly. A concrete example: if orders is later sharded on customer_id (Step 5), the Step 3 transaction now spans documents that may live on different shards (an order and a product don't share the shard key) — MongoDB's distributed transactions support this, but with added latency and failure modes single-shard transactions don't have. Design decisions made independently, chapter by chapter, still need to be re-verified together once the whole system is assembled — the same discipline this course's earlier capstone-adjacent chapters (Ch.4's transaction retries, Ch.6's scatter-gather gotcha) each flagged on their own.

🎉 Course Complete

That's the full MongoDB Intermediate/Advanced course — from the aggregation framework through schema design at scale, transactions, replication, sharding, performance and security, and finally a real application combining all of it. Together with MongoDB Fundamentals, this completes both MongoDB courses on the site.