00. Design an E-commerce Platform¶
~22 min read · Level: advanced · Part 1 of 4 (Overview → HLD → LLD → Q&A)
Problem¶
An e-commerce platform lets shoppers browse a catalog, put items in a cart, and pay, while the business tracks inventory and fulfills orders. This is the Amazon/Flipkart/Shopify shape. The interesting engineering is not "show products and take money" — it is doing that correctly when many people want the same scarce item at the same instant, and when a payment must move money exactly once even though networks drop and users double-click.
Thread one scenario through the design: a flash sale on a limited sneaker — 10,000 units, 500,000 shoppers arriving in the first minute, and checkout peaking at ~5,000 orders/minute. Overselling those 10,000 units, or charging a customer twice, is the failure everyone remembers. That contention, and that exactly-once money movement, will test every decision below.
Functional requirements¶
- Browse and search a catalog of products with prices, images, and availability.
- Add/remove items in a cart that survives across sessions and devices.
- Checkout: reserve inventory, take payment, create an order.
- Track order status through a lifecycle (placed → paid → shipped → delivered).
- Manage inventory as stock is sold, returned, or restocked.
De-scoped as deliberate choices: recommendations (its own case study), reviews/ratings, seller onboarding for a marketplace, and warehouse/logistics routing. These sit beside the core purchase flow and don't change its spine.
Non-functional requirements¶
The dominant constraint is correctness under contention on the write path — inventory and payment must never oversell or double-charge — layered on top of a read-heavy browse path.
- Latency: browse/search under ~200 ms; checkout can take a second or two, users tolerate a spinner when money is involved.
- Consistency: strong for inventory decrement and payment; eventual is fine for catalog reads, search freshness, and order-history views.
- Availability: browsing must stay up (a down storefront loses all sales); checkout can degrade to a queue before it fails.
- Durability: orders and payments are permanent financial records.
Scale estimation¶
Assume 50M catalog items, 10M daily active users, and 1M orders/day. Browse dominates: at ~50 page views per session and 10M sessions, that's 500M reads/day ≈ 5,800 reads/second average, ~30,000/s at peak. Orders average 1M / 86,400 ≈ 12/second, but a flash sale compresses a day's orders into minutes: our sneaker sale's 5,000 orders/minute is ~83 order-writes/second concentrated on one product row — the contention is not in aggregate throughput, it is on a single hot key.
Storage: 50M items × ~5 KB (text, attributes, image refs) ≈ 250 GB for the catalog, plus order history growing at 1M/day × ~2 KB ≈ 2 GB/day. Neither is large; the challenge is access pattern, not volume.
API sketch¶
GET /api/v1/products?q=…&category=… → paginated product cards
GET /api/v1/products/{id} → detail + live availability
POST /api/v1/cart/items {product_id, qty} → cart state
POST /api/v1/checkout {cart_id, payment_token, idempotency_key}
201: { order_id, status: "paid" }
409: { error: "out_of_stock", product_id }
GET /api/v1/orders/{order_id} → order + lifecycle status
Solutioning¶
Split the system by its two opposite traffic shapes. The browse path is read-heavy, tolerant of staleness, and wants to scale cheaply: serve it from a search index and heavy caching/CDN, backed by a catalog store. The checkout path is write-heavy on a few hot rows, demands correctness, and is where the design earns its keep. Keeping these two paths on separate stores and services means a flash sale hammering checkout can't slow down browsing, and vice versa.
The defining problem is inventory under contention. The naive "read stock, if > 0 then decrement" has a race: two requests both read 1, both decrement, and you've sold 10,001 of 10,000. The reframing: this is not a locking problem to be solved with a big mutex; it is an atomic-conditional-update problem. A single UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0 lets the database's row lock serialize the decrement, and the "rows affected = 0" result is the out-of-stock signal. For our sneaker, 500k requests funnel into that one conditional update; the first 10,000 that win the row lock succeed, the rest get a clean 409. The tradeoff is throughput on that hot row — serialized single-row updates cap at a few thousand/second — which is why high-contention sales often add a reservation queue or shard the counter (developed in the LLD).
The second defining problem is exactly-once payment. Checkout must be idempotent: the client sends an idempotency_key, and a retry with the same key returns the original result instead of charging again. Money movement itself is decoupled — reserve inventory synchronously, then capture payment, then emit an order-created event that fulfillment consumes asynchronously. The tradeoff is a brief window where inventory is reserved but payment hasn't settled; a timeout releases abandoned reservations back to stock.
The result: a browse tier that scales on read replicas and cache, a checkout tier built around atomic conditional updates and idempotency keys, and an order/fulfillment tier that runs asynchronously off an event log. The files below take each down to components, then to schemas and the reservation algorithm.