01. E-commerce Platform — High-Level Design¶
~16 min read · Part 2 of 4 (Overview → HLD → LLD → Q&A)
Architecture¶
┌──────────────┐
client ────────▶ │ CDN / edge │ (product images, cached cards)
└──────┬───────┘
▼
┌──────────────┐
│ API Gateway │
└──┬───────┬───┘
browse path │ │ checkout path
┌──────────┘ └──────────┐
▼ ▼
┌───────────────┐ ┌────────────────┐
│ Catalog svc │ │ Checkout svc │
│ + Search index│ │ (orchestrator) │
└───┬───────────┘ └──┬──────┬──────┘
│ │ │
┌────▼─────┐ ┌──────────┐ ┌─────▼──┐ ┌─▼────────┐
│ Catalog │ │ Redis │ │Inventory│ │ Payment │
│ DB (RO │ │ cache │ │ svc+DB │ │ svc(PSP) │
│ replicas)│ └──────────┘ └────┬───┘ └─┬────────┘
└──────────┘ │ │
┌──────▼───────▼──────┐
│ Order svc + DB │
└──────────┬───────────┘
▼
┌──────────────┐
│ Event bus │──▶ Fulfillment,
│ (Kafka) │ email, analytics
└──────────────┘
Components¶
API Gateway. Auth, rate-limiting, and routing. Splits the two traffic shapes so browse and checkout scale independently.
Catalog service + Search index. Owns product data. Reads are served from read replicas, a Redis cache of hot product cards, and a search index (Elasticsearch/OpenSearch) for queries and filters. The index is updated asynchronously from catalog writes — search results lagging a few seconds is acceptable.
Checkout service (orchestrator). Coordinates the critical write path: reserve inventory → capture payment → create order. It enforces idempotency and drives the saga that compensates if a step fails.
Inventory service + DB. The system of record for stock. Its only job that matters is the atomic conditional decrement; everything else (restock, returns) writes here too.
Payment service. Integrates the external payment processor (PSP). Turns a payment token into a capture, idempotently.
Order service + DB. Durable record of every order and its lifecycle state. Emits domain events on state changes.
Event bus. Decouples order creation from fulfillment, email, and analytics — none of which should be on the synchronous checkout path.
Primary write path (checkout)¶
POST /checkoutwith{cart_id, payment_token, idempotency_key}hits the checkout service.- The service checks the idempotency store: if this key already completed, it returns the stored result — no double action.
- It calls the inventory service to reserve each line item via atomic conditional update. Any item that fails returns
409 out_of_stockand the saga releases already-reserved items. - With inventory held, it calls the payment service to capture (also idempotent, keyed by the same idea).
- On payment success it creates the order row (status
paid), persists the idempotency result, and emitsOrderCreated. - Fulfillment, confirmation email, and analytics consume that event asynchronously. The user gets
201after step 5; nothing downstream blocks them.
Primary read path (browse)¶
- Product listing/search hits the search index (already denormalized for filtering and ranking).
- Product detail hits Redis first; on miss, a read replica of the catalog DB, then backfill cache.
- Live availability is a small, separate read from the inventory service (or a cached approximate count with an exact check deferred to checkout) so a stale catalog cache never blocks showing the page.
Storage choices¶
- Catalog: relational or document store with read replicas. Rich structured data, read-dominated; replicas absorb the 30k/s browse peak.
- Search: inverted index (Elasticsearch). Full-text and faceted filtering are what it's built for; kept eventually consistent with the catalog.
- Inventory: strongly-consistent relational rows. The atomic conditional decrement needs a real row lock and ACID guarantees; this is not a place for eventual consistency.
- Orders: relational, durable, never eventual. Financial records with lifecycle state and audit needs.
- Cache: Redis for hot product cards and cart state.
Scaling¶
Browse scales horizontally on read replicas, cache, and CDN — the 30k/s peak is comfortably absorbed and never touches the write path. Checkout aggregate throughput (83 orders/s in the sale) is trivial; the real limit is the hot inventory row. A single row's serialized conditional updates top out around a few thousand/second — fine for our sneaker's 5,000 orders/minute, but a truly viral item (a console launch at 50k orders/minute) exceeds it, and you then shard the counter: split 10,000 units into 10 buckets of 1,000, route requests across buckets, and sum for display. That trades exact-at-all-times availability display for ~10× decrement throughput.
Operational signals¶
The healthy signal is checkout success rate holding near its baseline as traffic climbs. The first metric to degrade in a sale is inventory-decrement latency (row-lock contention queues writers) — watch it, not CPU. The misleading metric is overall request rate: it can look fine while checkout is silently failing because browse traffic dwarfs it. The graph an operator opens first is 409/oversell-attempt rate versus units remaining: a healthy sale shows 409s rising smoothly to 100% exactly as stock hits zero; oversells (negative stock) or a flat 409 line while stock remains signal a decrement bug or a stuck reservation.
Failure modes and resilience¶
- Payment succeeds but order write fails. The saga must reconcile: either retry the order write (idempotent) or refund the capture. Never leave money taken without an order.
- Inventory reserved, payment never completes. A reservation TTL releases stock back after, say, 10 minutes, so abandoned carts in our sale don't permanently lock the 10,000 units.
- Search index lag. Products may appear available in search after selling out; checkout's exact conditional update is the backstop, returning 409 — search is advisory, inventory is authoritative.
- PSP outage. Degrade checkout to a queue ("we'll confirm shortly") rather than hard-failing, and hold inventory reservations until the PSP recovers.
- Hot-row browning out. Under extreme contention the counter shard split (above) is the lever; before that, a virtual queue admits shoppers to checkout at a rate the row can serve.
Where this shows up in production¶
- Amazon — separates the read-optimized product catalog from the strongly-consistent order/inventory services, so browsing survives when checkout is under load.
- Shopify — runs flash-sale "checkout throttling" (a virtual waiting room) to admit buyers at a rate inventory can serialize.
- Stripe — the idempotency-key pattern used here for double-charge protection is Stripe's public API contract.
- Elasticsearch at retail — powers faceted product search kept eventually consistent with the source catalog.
- Amazon DynamoDB — conditional writes (
ConditionExpression) are the NoSQL form of the atomic "decrement if > 0" used for inventory. - Kafka at commerce companies — the
OrderCreatedevent fan-out to fulfillment, email, and analytics is a canonical use. - PayPal/Adyen — external PSPs behind a payment service, integrated idempotently so retries don't re-capture.