Skip to main content

Capstone Pass 2: The Marketplace Data Layer

The Brief

You built this system once already — in Pass 1, as a single-region MVP: one load balancer, a few stateless app servers, a cache where it paid, one honest database, and a queue fanning out order events. It was correct, and it was just enough. Then it grew.

"Plates" — the food-delivery marketplace — has gone national. Call it ~20M daily users, forty metros, the same two sharp meal spikes a day. And here's the thing the Pass 1 review warned you about: the cracks that growth calls in are not load problems. Pass 1's envelope already sized the fleet, the cache, and the queue for far more traffic than this. The problems now are about the data itself:

  • The last pizza. Two customers order the final margherita in the same 50 ms. Today, both rows say confirmed. That's a correctness-under-concurrency bug, and it's shipping right now.
  • "Best sushi near me under ₹500." The catalog's WHERE name ILIKE '%sushi%' was fine for 4,000 restaurants. It is a full-table-scan nightmare for a national marketplace — and it can't rank, or filter by distance, at all.
  • Order history forever. Three years of orders is heading for ~0.9 billion rows — and analytics wants to query all of it fast, on the same table that's serving tonight's dinner rush.

Your deliverable is Pass 2 of the capstone: the data layer. Everything Container 2 taught — storage engines, replication and consistency, read scaling, transactions, partitioning, and the specialized indexes you just finished — aimed at one grown system.

The trailhead rule still stands: attempt before you peek. Grab paper, sketch your data layer and run the envelope before the guided build below. The build grades your instincts decision by decision; the reference afterward is for comparing, not copying.

The brief for Pass 2 of the marketplace capstone. Plates, the food-delivery marketplace built in Pass 1 as a single-region MVP, is now going national — roughly 20 million daily users. The picture frames the three cracks that growth calls in, and they are all data problems, not load problems. First, the last-pizza problem: two customers order the final item in the same instant and today both orders are confirmed — a correctness-under-concurrency failure. Second, search: “best sushi near me under 500 rupees” is a query the catalog's WHERE-name-LIKE cannot serve, an access-pattern problem. Third, order history forever: three years of orders, close to a billion rows, must be queryable fast while the same store serves tonight's dinner rush — a hot-versus-cold growth problem. The task is to design the data layer that fixes all three using Container 2: storage engines, replication and consistency, read scaling, transactions, partitioning, and specialized indexes.

Build It: Eight Decisions

Design reviews run in an order, and the order is the method: size the data, then let the shape of the load choose each store. Eight decisions. The first keeps the pass honest; the second is the envelope; the last six are one Container-2 section each — storage, transactions, replication, reads, partitioning, indexes.

Each choice is graded — the reference move, a defensible-but, or the trap it came from — and every wrong option is a trap this container named out loud. Watch the architecture draw itself as you decide.

Eight decisions, in design-review order, that turn Pass 1's one honest box into a data layer that survives a national marketplace. You'll pick the storage engine for two opposite workloads, stop the last-pizza race, place consistency only where it's seen, scale 40,000 read QPS off the primary, choose a shard key that won't run hot, and give search and “near me” their own index shapes — watching the architecture assemble itself, component by graded component.

The Reference: The Envelope & the Asymmetry

Compare, don't copy. Here's the reference reasoning for the numbers — the part that decides everything after it.

The envelope. 20M DAU × ~40 requests = 8×10⁸ requests/day. A day is ~10⁵ seconds, so that's ~8,000 QPS average; food earns the top of the peak rule (×5), so ~40,000 read QPS at the meal rush. Orders are ~4% of users — ~800K/day, about 40 order-writes/sec at peak, roughly 1,000× fewer than the reads. And three years of orders at ~800K/day is ~0.9 billion rows.

Now read the asymmetry, because it is the design. Those three numbers don't just size hardware — they point in three different directions:

  • Reads are enormous and repetitive (everyone browses the same popular menus) → they want a cache and read-replicas, and they can tolerate being a little stale.
  • Writes are tiny but sacred (40/sec, but each is money and inventory) → they want a transaction, and they cannot be stale to the person who made them.
  • History is huge and mostly cold (0.9 B rows, but tonight's rush only touches today's) → it wants partitioning and hot/cold tiering.

The load isn't one shape, so the data layer isn't one store. Every decision below is really just: which of these three pressures am I answering right now?

The envelope for Pass 2, and the asymmetry that drives every decision. Twenty million daily users at about forty requests each is eight hundred million requests a day; divided by the hundred thousand seconds in a day that is eight thousand queries per second on average, and with the meal-rush peak of five times, about forty thousand read queries per second at the top. Orders are only about four percent of users, roughly eight hundred thousand a day, which is about forty order-writes per second at the peak — a thousand times fewer than the reads, but each one must be exactly correct. And three years of orders is close to nine hundred million rows. The three numbers point in three directions: reads are enormous and repetitive, so they want a cache and replicas; writes are tiny but sacred, so they want a transaction; history is huge and mostly cold, so it wants partitioning and tiering. The shape of the load decides the shape of the data layer.

The Reference: The Data Layer

Here's the reference architecture — the six choices, assembled. Yours doesn't have to match it; it has to be defensible with numbers, the way this one is.

Storage engine. The append-only order/event firehose goes on an LSM-tree (sequential writes, no in-place update, no write amplification); the read-mostly catalog stays on a B-tree (fast point and range reads). Match the engine to the access pattern — one size fits neither.

The last pizza. The inventory decrement is a read-modify-write, so it's wrapped in a transaction that locks the item row (SELECT … FOR UPDATE, or serializable isolation) — the second order sees the decremented count and is refused. Crucially, that cost is scoped to the money + inventory path only; browsing never touches it.

Replication & consistency. A single leader takes writes. The money path is synchronous / read-your-writes (you always see the order you just placed); browse is served from asynchronous read-replicas where a few seconds of staleness is invisible. Consistency is paid in latency — so it's spent only where a human would notice.

Reads. 40,000 QPS never touches the primary head-on: a catalog cache (short TTL) absorbs the repeated menu reads, replicas carry the browse tail, and only writes plus must-be-fresh reads hit the leader.

Partitioning. The write firehose and 0.9 B rows are sharded by hash(user_id) so load spreads evenly (never by city — the biggest metro would melt one shard), and history is partitioned by time for hot/cold tiering.

Access patterns. The queries the B-tree can't answer each get their own shape: an inverted index + BM25 for search, a geohash / S2 index for “near me”, and a sketch for “trending”. §8, applied.

Every box earns its place from a number or a failure mode. That's the whole method.

The reference data layer for Plates at national scale. Customers hit stateless app servers. Reads, which dominate, are served two ways: a cache holds the hot catalog with a short time-to-live, and asynchronous read-replicas carry the long tail of browse traffic, while reads that must be fresh — a customer seeing their own just-placed order — are pinned to the leader. Writes go to a single leader whose order and event log uses an LSM-tree storage engine suited to an append-only firehose, while the read-mostly catalog stays on a B-tree. The leader is sharded by a hash of the user id so load spreads evenly across shards rather than piling onto one hot city, and the order history is partitioned by time so recent orders stay hot and old ones move to cold storage. The last-unit race is stopped by a row-locked, serializable transaction scoped only to the money-and-inventory path. And the queries a B-tree can't answer each get their own index: an inverted index with BM25 for search, and a geohash or S2 index for proximity. Money path synchronous and strongly consistent; browse path cached and eventually consistent.

Grade Yourself: The Pass 2 Rubric

Score your paper attempt — the sketch you made before the guided build — honestly. One point each:

The numbers (3):

  1. Sized the envelope: ~40,000 read QPS at peak, ~40 order-writes/sec, ~0.9 B rows over three years.
  2. Named the read/write/history asymmetry out loud — reads huge & repetitive, writes tiny & sacred, history huge & cold.
  3. Let that asymmetry drive the stores, rather than reaching for one database to do everything.

The shape (6):
4. Matched the storage engine to the workload (LSM for the write log, B-tree for the catalog) — not one engine for both.
5. Stopped the last-pizza race with a row-locked / serializable transaction, and scoped it to the money path only.
6. Put read-your-writes on the money path and eventual consistency on browse — not everything-strong or everything-eventual.
7. Scaled reads with cache + replicas, left order status uncached, and pinned fresh reads to the leader.
8. Sharded by a high-cardinality key (hash of user/order), not by city or status — and could say why city runs hot.
9. Gave search, “near me”, and trending their own index shapes instead of more B-tree indexes or a LIKE.

The judgment (3):
10. Nothing in the design exists without a number or a failure mode justifying it.
11. Named the next ceiling — multi-region and real-time — without pre-building it.
12. Could walk another engineer through all of it in ~10 minutes.

10–12: staff-shaped. 7–9: solid pass — reread the reference tier that dropped points. Below 7: genuinely fine — reread The Reference against your sketch, mark the three biggest gaps, and redo it on a fresh sheet. A draft you correct teaches more than an answer you read.

What Pass 3 Will Break

This data layer is correct — and, like Pass 1's, it's carrying debts that the next stage will call in. The tell: every problem left is now about distribution, not data.

  • One region is one point of failure. Everything here lives in a single datacentre. One regional outage takes Plates fully down — and the moment you copy it to a second region, the CAP tax you priced in the consistency lessons becomes a geography bill you pay on every write.
  • The live map you deferred. Courier GPS every few seconds is a real-time fan-out — persistent connections, pub/sub — the one thing a request/response data layer can't serve.
  • Coordinating across shards and regions. A cross-shard transaction, a global leader election, exactly-once on the event stream — these need consensus, not just a lock on one row.

Pass 2's problems were data — correctness, access patterns, growth. Pass 3's are distribution — geography, real time, coordination. That's the next container.

Container 2, Closed: The Toolbox You Leave With

Look back at what your eight decisions quietly used — the whole of Container 2: Data Systems.

  • Storage engines — LSM vs B-tree, and matching the engine to an append firehose or a read-mostly catalog.
  • Transactions & concurrency — isolation levels, row locks, and the check-then-write race that oversells the last unit.
  • Replication & consistency — single-leader topology, sync vs async, and read-your-writes; consistency spent only where it's seen.
  • Scaling reads — caches, read-replicas, and the replication lag that makes “where's my order?” read stale.
  • Scaling writes & partitioning — sharding by a high-cardinality key, and the hot-shard / celebrity problem that a city key walks straight into.
  • Specialized indexes & sketches — §8's toolbox: an inverted index and BM25 for text, a spatial index for proximity, a sketch for frequency — each access pattern its own shape.

You started Container 1 with one honest box. You leave Container 2 able to take that box apart into the right stores — one for each pressure the data puts on it — and defend every one with a number. Next, in Container 3, that data layer has to survive being spread across the planet.