Blueprint
Money & markets advanced

Design Amazon

Everyone underestimates this one — it's four systems (catalogue, cart, inventory, checkout) welded together, each with a different consistency story, all failing to converge at Black Friday.

asked at Amazon · Shopify · Flipkart Sometimes asked ~40 min walkthrough
01

Scope the requirements

This is where candidates who treat the whole thing as one service lose the interview. The four functional areas — browsing, cart, inventory reservation, checkout — have fundamentally different consistency and availability needs. Draw the seams first.

Functional — what it must do

  • Browse and search a catalogue of hundreds of millions of products.
  • Add to cart, persist across devices and sessions.
  • Show real inventory (or a fuzzy 'in stock' signal) with price.
  • Place order: reserve stock, charge, produce shipment. No overselling.
  • Explicitly cut: reviews, recommendations, seller onboarding — offer them back.

Non-functional — the numbers that shape the design

  • Read-heavy: browse is 1000:1 read-to-write across the catalogue.
  • Peak QPS: 1M product-page views/s, 100k cart writes/s, 5k checkouts/s on Black Friday.
  • Catalogue read latency under 100 ms p95; checkout under 500 ms end-to-end.
  • Strong consistency on inventory — zero oversells even at 5k concurrent buys of the same SKU.
  • Availability: browse survives everything; checkout may degrade to "try again" before it goes down.
Say this out loud

I'll design four subsystems with different SLAs and let a small orchestrator glue them at checkout. That gives me strong consistency exactly where money moves, and eventual consistency everywhere I can afford it.

02

Back-of-envelope estimates

The catalogue read is the biggest number by two orders of magnitude, so it must be cache-first. The checkout is the smallest but has the harshest consistency requirement.

QuantityEstimateHow you got there
Product-page reads~1M/s peak500M active shoppers × ~5 pages/session in a 30-minute window on peak day.
Cart writes~100k/s peakadds/removes/quantity changes; one write per user action, batched by client.
Checkouts~5k/s peaksuccessful orders — small compared to reads because most sessions don't convert.
Inventory writes~5k/s peakone decrement per SKU per successful order line.
Catalogue storage~1 TB metadata500M SKUs × ~2 KB (title, desc, attrs, price, images-refs).
Search index~10 TBpostings + n-grams for typeahead; sharded across ~50 nodes.
Say this out loud

1M reads a second on the product page means Redis + CDN eat 99% of it; the catalogue database sees maybe 10k/s of misses. The heavy lifting is caching, not databases.

03

Sketch the API

Four small APIs, one for each subsystem. The interesting decision is who owns the pre-checkout reservation — the cart or a dedicated inventory service.

GET/api/v1/products/{id}→ product page. Served from Redis; catalogue DB on miss.
POST/api/v1/cart/itemsbody: { sku, qty }. Idempotent per (user, sku). No inventory check here — cart is aspirational.
POST/api/v1/checkoutbody: { cartId, address, payment }. Runs the reservation → charge → order-create saga.
GET/api/v1/inventory/{sku}→ { available, price }. Cheap approximate read for badges; not the authoritative source for reservation.
  • Cart is not a reservation. Adding to cart doesn't touch inventory — the SKU can sell out while it sits there, and the user sees a message at checkout. This is the industry norm and it's the only design that scales.
  • Checkout is a saga, not a distributed transaction. Reserve inventory → charge payment → create shipment record, with compensating actions if a later step fails.
04

Data model

Different subsystems, different stores. Catalogue is document-shaped and mostly-read; cart is session state; inventory is a single counter per SKU per warehouse.

products
id PK · title · attrs (json) · price · category · seller_id
DynamoDB or Cassandra — key by product_id, document body. Heavily cached in Redis + CDN.
inventory
(sku, warehouse_id) PK · available · reserved · updated_at
Postgres with row-level locking, sharded by sku hash. Small hot rows on flash sales.
carts
user_id PK · items[] (sku, qty, added_at)
Redis (primary) + async persistence to Cassandra for durability across devices.
orders
id PK · user_id · items[] · status · payment_ref · shipment_id
Postgres, one shard per region. The record of truth for money and shipment.
The database call

The database choice happens per-subsystem: DynamoDB for the catalogue's key-by-id + document body access pattern; Postgres for inventory and orders where I need a row lock and a transaction; Redis-first for cart because the access pattern is per-session and reads dwarf writes. I'd say this out loud as four decisions, not one.

05

High-level design

Four boxes doing four jobs, glued by a checkout orchestrator that runs the saga. Everything in front of the orchestrator scales like a website; everything the orchestrator touches is transactional.

client edge service data async Shopper CDN images, static API gateway Catalog svc Cart svc Checkout saga orchestrator Inventory svc Redis products, carts DynamoDB catalog Postgres inventory, orders Elasticsearch Payments Shipping Event bus get reserve images order miss charge search
the four subsystems in the middle row don't share state. checkout is the only place they meet.
  1. Product page load: user → API → catalogue service → Redis → response. 99% ends here. Images come off the CDN.
  2. Add to cart: → cart service → Redis (primary), async replicate to Cassandra for cross-device durability. Inventory not touched.
  3. Checkout initiated: orchestrator freezes the cart, calls inventory service to reserve stock for every line item — that's the strong-consistency moment, done in Postgres with row-level SELECT ... FOR UPDATE per SKU.
  4. Reservation success → orchestrator calls the payment gateway with an idempotency key; on charge success, order row is inserted, event emitted.
  5. Any failure at reservation or charge triggers compensating actions: release the reservation, refund if charged, message the client. Order state moves through PENDING → CONFIRMED → SHIPPED via events.
06

Deep dives — where the interview is won

Phase 01

Inventory: the only strongly-consistent part of the system

Every other subsystem can be eventually consistent. Inventory can't — two customers can't both buy the last one. The move: one row per (sku, warehouse) in Postgres, sharded by sku hash so hot SKUs distribute across primaries. Reservation runs a small transaction: SELECT available FOR UPDATE; UPDATE SET available = available - qty, reserved = reserved + qty WHERE available >= qty; COMMIT. If the CAS fails, the client sees "out of stock" and can either wait or drop from cart.

For a Black Friday drop of a single hot SKU, even a single row can bottleneck at ~1k transactions per second. The fix used at scale: split the SKU's inventory into virtual buckets across N rows (SKU_A_bucket_0 … SKU_A_bucket_9), and hash incoming requests across them. Total inventory is the sum; individual reserves scale N-fold. When a bucket runs out, the request retries against a random other bucket.

service inventory shard Checkout SKU-A b0 SELECT FOR UPDATE SKU-A b1 SKU-A b2 retry reserve retry
one hot SKU split across N buckets. every reservation hits one bucket; retries fan out.
Trade-offBucketing loses global "how much is left" as a strongly-consistent number — you approximate it by summing rows read at read-committed. Perfectly acceptable because the badge is fuzzy anyway ("only 3 left!").
Phase 02

The checkout saga — compensating actions instead of distributed transactions

A 2-phase commit across inventory, payments and shipping is impossible in practice (the payment gateway is a third-party HTTP API — it doesn't participate in your transaction manager). The pattern is a saga: each step is local, each has a documented undo. Reserve inventory (undo: release), charge payment (undo: refund), create order (undo: mark cancelled), notify shipping (undo: cancel shipment).

The orchestrator holds the state machine and drives the sequence, persisting state to Postgres after each step so a crash mid-saga resumes correctly. Each downstream call carries an idempotency key so retries don't double-charge. The key insight for the interview: sagas trade the strong-consistency guarantees of 2PC for durability of intent — the payment can succeed while the order write fails, and reconciliation catches it later. That's the price of talking to a system you don't own.

orchestrator steps Checkout saga state in Postgres 1. Reserve 2. Charge 3. Order 4. Ship step undo?
orchestrator persists state after each step; a crash resumes exactly where it left off, and every step has an undo.
Trade-offSagas expose intermediate states — a customer can see "payment succeeded but order not visible yet" for seconds. Handle it in UI ("processing") rather than pretending checkout is atomic.
Phase 03

Catalogue reads: cache-first, database on the tail

1M product-page reads per second is a Redis problem, not a database problem. Product pages are cached whole (id → serialised product JSON) with a 30-minute TTL; hot pages effectively live in cache forever because every read refreshes them. The catalogue database is read-through: cache miss → DynamoDB GetItem → populate cache. Even at peak, only the long tail of never-viewed SKUs hits the database.

The subtle problem is price changes. If a seller updates a price, the cache still holds the old value for up to 30 minutes. Solutions in order of complexity: rely on TTL and accept the lag; publish invalidation events to a bus that the cache layer subscribes to; use a change-stream from DynamoDB to purge specific keys. For Amazon the last option wins because catalogue write volume is small and correctness matters — the CDC pipeline is the same one that also feeds the search index.

client service cache source Shopper Catalog svc Redis CDN images DynamoDB CDC GET hit invalidate images miss
hits go to Redis, misses fall through to DynamoDB, and CDC purges keys on updates so price changes propagate in seconds.
Trade-offCDC-driven invalidation adds a real dependency — a lag in the CDC pipeline lets stale prices linger. Belt-and-braces: keep a TTL of an hour so even a broken CDC self-heals.
07

Follow-ups you should expect

What happens when a cart item is out of stock at checkout?
The reservation step fails. Orchestrator returns a structured error to the client naming the SKU; the cart drops that line item, offers alternatives from the catalogue, and the user retries checkout with what remains. This is not a rare edge case — it's expected behaviour that has to be first-class in the UI.
How do you keep the cart across devices?
Cart is keyed by user_id, stored primarily in Redis for read speed. Every mutation also writes-through to a persistent store (Cassandra keyed by user_id) asynchronously. On a device switch, the Redis replica set in the user's region already has it; on a Redis eviction, we rehydrate from Cassandra.
Why not just do checkout in a single database transaction?
Because charging a card is an HTTP call to a system you don't control. You can't hold a database transaction open across a network call to Stripe — you'd starve the pool and block on their latency. Sagas exist precisely to compose local transactions with external systems.
How do you rank search results without recomputing on every query?
Elasticsearch does the retrieval, ranking is a mix of ES BM25 + a learned reranker over the top-100. Business signals (sponsored, in-stock, prime-eligible) are precomputed and stored as doc fields; user-specific personalisation is a small on-request boost using precomputed profile embeddings from the recs pipeline.
What's the disaster case — one Postgres shard for a hot SKU dies mid-Black-Friday?
Failover replica takes over; in-flight reserves that were mid-transaction abort and the orchestrator returns "try again". The bucketed inventory design means a single shard death degrades that SKU (partial capacity), not the whole store. Alarms fire; ops rebalances buckets away from the failed node.

Where candidates lose the room

  • Designing one service that handles catalogue, cart and checkout — you inherit the strongest consistency requirement on every read.
  • Reserving inventory when the item is added to cart — carts are aspirational, and locking stock this early destroys conversion.
  • Reaching for 2-phase commit at checkout — the payment gateway isn't in your transaction manager, so 2PC is fiction.
  • Ignoring the hot SKU problem — one row can't sustain 5k reservations/sec on Black Friday and no amount of read replicas helps.
  • Coupling search index freshness to catalogue writes with a synchronous update — every price change now waits on Elasticsearch.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.