Skip to content

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)

  1. POST /checkout with {cart_id, payment_token, idempotency_key} hits the checkout service.
  2. The service checks the idempotency store: if this key already completed, it returns the stored result — no double action.
  3. It calls the inventory service to reserve each line item via atomic conditional update. Any item that fails returns 409 out_of_stock and the saga releases already-reserved items.
  4. With inventory held, it calls the payment service to capture (also idempotent, keyed by the same idea).
  5. On payment success it creates the order row (status paid), persists the idempotency result, and emits OrderCreated.
  6. Fulfillment, confirmation email, and analytics consume that event asynchronously. The user gets 201 after step 5; nothing downstream blocks them.

Primary read path (browse)

  1. Product listing/search hits the search index (already denormalized for filtering and ranking).
  2. Product detail hits Redis first; on miss, a read replica of the catalog DB, then backfill cache.
  3. 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 OrderCreated event 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.