System designAdvanced

Design an E-commerce Platform

Catalogue, search, cart, checkout, inventory, payments and order fulfilment. The interesting parts are the ones where money and stock meet retries: reserving the last unit, charging exactly once through a provider that times out, and keeping an order’s state honest while five downstream systems react to it.

Functional requirements

  • Browse the catalogue by category; full-text search with filters (price, brand, availability).
  • Cart: add/remove items, persists across devices and sessions, merges on login.
  • Checkout: reserve stock, take payment through an external provider, create the order — never oversell, never double-charge.
  • Order lifecycle: created → paid → picked → shipped → delivered, with cancellation and refund rules per state.
  • Downstream reactions: confirmation email, warehouse pick list, analytics, fraud review — without blocking checkout.
  • Customers see their orders and status in real time; support can see the full history of an order.
  • Provider webhooks (payment succeeded/failed, refund settled) update the order reliably.

Non-functional requirements

Scale, latency, availability and durability targets — these decide the architecture.

  • Product page p99 < 200 ms; search p99 < 500 ms; checkout API p99 < 2 s excluding the provider’s own latency.
  • Scale: 10M orders/day on peak days, 1B product page views/day, 5M SKUs.
  • Correctness over availability for stock and money: refuse a checkout rather than oversell or double-charge.
  • Availability 99.95% for browse and cart; 99.9% for checkout (the provider is the weakest link).
  • Order data retained 10 years for accounting; every state change is auditable.

Back-of-the-envelope

Numbers first. Every component below has to be justified by one of these.

QuantityValueArithmetic
Checkout rate≈ 116 /s avg, ~1,200 /s in a flash sale10M orders/day ÷ 86,400 ≈ 116/s; a flash-sale peak of 10× ≈ 1,200/s. Each checkout is ~5 DB writes in one transaction → 6k writes/s, well within one primary.
Catalogue reads≈ 11,600 /s avg, 100k /s peak1B page views/day ÷ 86,400 ≈ 11,600/s; peak ~100k/s during a sale. 100:1 read:write on products → cache + CDN, not database replicas, absorb it.
Payment concurrency≈ 2,400 in-flight calls at peakLittle’s law: 1,200 checkouts/s × ~2 s provider latency = 2,400 concurrent provider calls. That is the bulkhead size the checkout service must reserve — and what a 30 s provider timeout would multiply into 36,000 stuck threads.
Inventory5M SKUs × ~200 B ≈ 1 GBInventory fits in memory and one table. The problem is contention on hot SKUs (one row updated 1,000×/s in a flash sale), not size.
Order storage≈ 50 GB/day, 18 TB/yr10M × ~5 KB (order, items, addresses, payments, state history) = 50 GB/day. Partition by month; move orders older than 2 years to cold storage while keeping them queryable.
Cart≈ 50M active carts, ~50 GB in Redis50M carts × ~1 KB (hash of sku → qty, TTL 30 days) ≈ 50 GB; a small Redis cluster, persisted so a restart does not empty everyone’s cart.

Interface

Endpoints, messages or events.

GET /products/{id} → { sku, name, price, stock_status, … }Cached at the CDN (60 s) and in Redis; stock_status is coarse (in stock / low / out) so the page can be cached — the exact count is checked at checkout, not here.
GET /search?q=&category=&price_lt=&cursor= → { hits[], facets, next_cursor }Served by the search index, never by LIKE on the products table. Results may lag catalogue changes by seconds.
PUT /carts/{cart_id}/items/{sku} { qty } → 200 { cart }PUT = set quantity, naturally idempotent (retry sets the same value). cart_id is a cookie for guests and merges into the user’s cart on login.
POST /checkout { cart_id, address, payment_method_id } (Idempotency-Key header) → 202 { order_id, status: "payment_pending" }The core command. The key is client-generated (UUID per checkout attempt); a retry with the same key returns the same order_id and never creates a second order or charge. Returns 202: payment confirmation arrives asynchronously.
GET /orders/{id} → { status, items, payment, timeline[] }Reads the order and its state history; the client polls or subscribes (SSE) for paid.
POST /orders/{id}/cancel → 200 | 409Allowed only in payment_pending/paid; 409 with the current state otherwise. Cancelling a paid order triggers the refund saga.
POST /webhooks/payments (provider → us)Verify the HMAC signature, dedup on event_id, persist, return 200 within 5 s; process from a queue. Events can arrive out of order and more than once.
POST /products/{id}/inventory { delta, reason } → 200Warehouse adjustments; appends to an inventory ledger rather than overwriting the count.

Build it one problem at a time

Each step names the problem first. Decide what you would add before revealing the reference answer.

1
Start as a modular monolith with one database
Problem · A checkout touches cart, inventory, payment and order. In one process with one Postgres this is a single transaction: decrement stock, insert order, record payment intent, commit. Splitting into services on day one turns that transaction into a distributed-systems problem before there is any traffic to justify it.
Work through every step to unlock the data model, the request walkthrough, scaling, failure modes and the open decisions.