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.
| Quantity | Value | Arithmetic |
|---|---|---|
| Checkout rate | ≈ 116 /s avg, ~1,200 /s in a flash sale | 10M 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 peak | 1B 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 peak | Little’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. |
| Inventory | 5M SKUs × ~200 B ≈ 1 GB | Inventory 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/yr | 10M × ~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 Redis | 50M 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.