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.
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.
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.
| Quantity | Estimate | How you got there |
|---|---|---|
| Product-page reads | ~1M/s peak | 500M active shoppers × ~5 pages/session in a 30-minute window on peak day. |
| Cart writes | ~100k/s peak | adds/removes/quantity changes; one write per user action, batched by client. |
| Checkouts | ~5k/s peak | successful orders — small compared to reads because most sessions don't convert. |
| Inventory writes | ~5k/s peak | one decrement per SKU per successful order line. |
| Catalogue storage | ~1 TB metadata | 500M SKUs × ~2 KB (title, desc, attrs, price, images-refs). |
| Search index | ~10 TB | postings + n-grams for typeahead; sharded across ~50 nodes. |
1M reads a second on the product page means Redis + CDN eat 99% of it; the catalogue database sees 10k/s of misses. The heavy lifting is caching, not databases.
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.
- 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.
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.
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.
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.
- Product page load: user → API → catalogue service → Redis → response. 99% ends here. Images come off the CDN.
- Add to cart: → cart service → Redis (primary), async replicate to Cassandra for cross-device durability. Inventory not touched.
- 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.
- Reservation success → orchestrator calls the payment gateway with an idempotency key; on charge success, order row is inserted, event emitted.
- 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.
Deep dives — where the interview is won
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.
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.
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.
Follow-ups you should expect
What happens when a cart item is out of stock at checkout?
How do you keep the cart across devices?
Why not do checkout in a single database transaction?
How do you rank search results without recomputing on every query?
What's the disaster case — one Postgres shard for a hot SKU dies mid-Black-Friday?
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.