Design an e-commerce cart and inventory system
An online shop on an ordinary day: millions browsing, carts filling and being forgotten, and a few thousand orders an hour. The flash sale (D24) is about surviving one extreme moment. This design is about the everyday hard parts: never selling stock you don't have, never losing a cart, and making checkout a sequence of steps that can fail halfway without leaving a mess.
1. The prompt
"Design the shopping cart, inventory and checkout for a large online store."
What makes it hard:
- Two kinds of consistency in one product: a product page can show "in stock" a few seconds late; checkout must never sell the last unit twice.
- Checkout spans several services (inventory, payment, orders) that can each fail independently.
- Most carts are abandoned, so anything held for a cart must be let go.
2. Frame the problem
How big is the store?
About 10 million products (SKUs), 50 million daily visitors, 1 million orders a day, peaks of 5× on holidays.
Reads (browsing) outnumber writes (orders) by hundreds to one: the catalogue is a caching problem; checkout is a correctness problem.
Guest carts?
Yes; a guest cart is merged into the account cart on login.
Carts are keyed by a session id until login, then by user id, and merging must be defined.
When is stock taken from inventory?
Reserved when checkout starts, for a limited time; committed when payment succeeds; released on cancel or timeout.
Adding to the cart doesn't hold stock: most carts are abandoned, and holding stock for them would hide it from real buyers.
Multiple warehouses?
Yes, about 20; an order may ship from the nearest one that has everything.
Stock is per SKU per warehouse; "available" on the site is a sum.
Overselling allowed?
No for normal items; some items allow backorders with a clear message.
The reservation must be a conditional update that can't go below zero.
Prices change. Which price does the buyer pay?
The price shown at checkout, honoured for a short time.
The order stores a price snapshot; the cart stores product ids, not prices.
Payments?
An internal payment service (D16) that authorises and captures.
We integrate with it idempotently; we don't design it again.
3. What it must do
- A shopper can add items to a cart, as a guest or logged in, from any device, and the cart is still there tomorrow.
- A shopper can check out: stock is reserved, payment taken, an order created, exactly once.
- Everyone sees availability on product pages, close to real time.
Not now: search and recommendations, the payment system itself (D16), shipping and returns, the flash-sale extremes (D24), marketplace sellers.
4. How well it must do it
| Target | Why this number |
|---|---|
| Never oversell a non-backorder item consistency | Cancelling a paid order is the worst customer experience in retail. |
| Cart writes always succeed (99.99%), even during partial outages surviving failures | A failed "add to cart" is a lost sale; the Dynamo paper used exactly this example. |
| Checkout exactly once: no double charges, no orders without payment, no payment without an order data safety | Retries and timeouts happen on every flaky phone network. |
| Product pages: availability no more than ~10 s stale; pages under 200 ms p99 speed | Fast browsing sells; a slightly stale "in stock" is acceptable because checkout re-checks. |
| Holiday peaks of 5× without degradation load shape | The peak days are the year's revenue. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Product page views | 50 M visitors × 20 pages ≈ 1 B a day ≈ 12,000/s, 60,000/s at peak | → so availability is served from a cached read model, not the inventory database |
| Cart writes | Maybe 5 per visitor ≈ 250 M a day ≈ 3,000/s, 15,000/s at peak | Simple key-value writes → so carts live in a partitioned key-value store keyed by cart id |
| Checkouts | 1 M orders a day ≈ 12/s, 60/s at peak (×3 items) | Low rate, high stakes → so checkout can afford strong consistency and a durable workflow |
| Abandonment | ~70% of started carts never buy | → so stock is reserved at checkout, not at add-to-cart, with a short timeout |
Illustrative numbers. The abandonment rate is in the commonly reported range for online retail.
6. The nouns
- Product (catalogue) and SKU (a sellable variant).
- Cart and cart lines.
- Stock per SKU per warehouse, and reservations against it.
- Order with lines, price snapshot and a status.
cart cart_id (session or user) → lines[(sku, qty)], updated_at (key-value, TTL 30 days) stock (sku, warehouse) → on_hand, reserved, version (relational, sharded by sku) reservation reservation_id, checkout_id, sku, warehouse, qty, expires_at, state (held | committed | released) order order_id, user_id, checkout_id (unique), lines[(sku, qty, unit_price)], total, status outbox event_id, aggregate, type, payload, published_at (same database as orders)
available = on_hand − reserved. A reservation succeeds only if it stays ≥ 0.
7. The contract
GET /v1/products/{id} → {…, availability: "in_stock" | "low" | "out"}
PUT /v1/cart/lines/{sku} {qty} → cart (cart id from session or token)
DELETE /v1/cart/lines/{sku} → cart
POST /v1/checkouts {} Idempotency-Key: …
→ 201 {checkout_id, lines, prices, total, expires_at} (stock reserved)
POST /v1/checkouts/{id}/pay {payment_method} Idempotency-Key: …
→ 200 {order_id, status: "confirmed"} | 409 {reason: "reservation_expired" | "payment_declined"}
The user comes from the session token. Every state-changing call takes an idempotency key, so a retried tap on "Pay" can't create a second order.
Data flow: cart → start checkout (snapshot prices, reserve stock) → authorise payment → create order and commit reservations (one transaction, plus an outbox event) → capture payment → events to fulfilment, email and the availability read model.
8. First working version
A catalogue service behind caches, a cart service on a key-value store, an inventory service on a relational database, an order service, and the payment service.
8.1 Adding to the cart
A write to the cart store keyed by the cart id; no inventory involved. The page shows availability from the read model. On login, the guest cart's lines are merged into the user's cart (sum quantities, cap at stock limits) and the guest cart deleted.
8.2 Checkout
Starting checkout reserves every line atomically per SKU (a conditional update: UPDATE stock SET reserved = reserved + q WHERE sku = ? AND on_hand - reserved >= q). If any line fails, the others are released and the shopper sees which item ran out.
8.3 Abandoned checkouts
A sweeper releases reservations past their expiry. The paying step re-checks expiry, so a slow payment after the timeout is rejected cleanly or re-reserves if stock is still free.
9. Make it hold up
9.1 When to hold stock, and for how long never oversell
| Approach | Why | |
|---|---|---|
| ✗ Bad | Read the count, then decrement at payment, with no condition | Two buyers read "1 left" and both pay: an oversell. |
| ~ Good | Conditional decrement at payment time only | Never oversells, but a shopper fills in a form for five minutes and is told at the end that it's gone. |
| ✓ Great | Reserve with a conditional update at checkout start, with a timeout a little longer than paying usually takes (measure it: often a few minutes); commit on payment; release on cancel or expiry | No oversell, and the buyer knows early. The timeout is the tuning knob: too short and payers lose their hold, too long and abandoned checkouts hide stock. Try it in the lab. |
Rows are sharded by SKU, so contention is per SKU. A very hot SKU becomes the flash-sale problem: see D24 for queues and pre-split stock.
9.2 Checkout as a saga exactly once
| Approach | Why | |
|---|---|---|
| ✗ Bad | One distributed transaction across inventory, payment and orders | Two-phase commit across services and an external payment provider: slow, fragile, often impossible. |
| ~ Good | A sequence of calls from the checkout service, hoping each succeeds | A crash between "payment captured" and "order created" leaves money taken with no order. |
| ✓ Great | An orchestrated saga in a durable workflow (Module 22): reserve → authorise payment → create order and commit reservations in one local transaction with an outbox event → capture payment; each step has a compensation (release, void authorisation, cancel order) | Every crash point has a defined recovery; the workflow resumes where it stopped. Authorise-then-capture means no money moves until the order exists. |
9.3 Retries and double taps exactly once
The phone times out after "Pay" and the shopper taps again. The idempotency key on the request maps to the same checkout, and checkout_id is unique on orders, so the second request returns the first result. Calls to the payment service carry the same key, so a retried authorisation isn't a second charge. Inventory commits are keyed by reservation id and are no-ops the second time.
9.4 Availability for 60,000 page views a second product pages
| Approach | Why | |
|---|---|---|
| ✗ Bad | Query stock for every page view | Product pages hammer the database that checkout depends on. |
| ~ Good | Cache counts with a TTL | Cheap, but popular items flip between in and out of stock unpredictably. |
| ✓ Great | A read model fed by inventory events (CQRS): every reserve, commit and release publishes an event (via the outbox); a projection keeps a coarse status per SKU ("in stock", "only 3 left", "out") in a cache | Product pages never touch the write database; staleness is seconds, and checkout's reservation is the real check. |
9.5 Carts that always accept writes cart writes
Carts are per user, small, and rarely contended: a partitioned, replicated key-value store fits. Amazon's Dynamo paper describes choosing availability for the cart: accept the write even during failures, and merge divergent versions later (union of lines), occasionally resurrecting a deleted item. Today, a store with single-key strong consistency per partition plus a cache usually suffices; mention the trade-off either way. Expire guest carts after a period; keep logged-in carts longer.
9.6 Many warehouses never oversell · cost
Availability on the page is the sum across warehouses that can ship to the shopper's region. At checkout, an allocation step chooses warehouses (nearest that has every item, else split shipments with a cost rule) and reserves there. If a warehouse's count turns out wrong (a damaged item), the order is re-allocated or the line cancelled, which is why physical counts and the system are reconciled daily.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
An item's price changes while it's in someone's cart.
The cart stores ids, not prices, so it always shows the current price. Checkout snapshots prices and honours them until the checkout expires; the order stores the snapshot.
Payment succeeds but the order service is down.
That's why we authorise first and capture last: the durable workflow retries order creation; if it can't complete within the authorisation window, it voids the authorisation and releases stock. No money moves without an order.
Inventory counts drift from what's on the shelves.
Warehouses send adjustments as events (damaged, found, returned); daily cycle counts reconcile; a safety buffer per SKU can hide the last unit or two of high-error items.
A celebrity tweets a product link.
Product pages are served from caches and the read model, so browsing is fine; checkout for that SKU contends on one row. Switch it to the flash-sale path (D24): a queue in front of reservation, or stock pre-split into buckets.
A shopper adds 1,000 of a cheap item to block others.
Per-SKU purchase limits checked at reservation, reservation timeouts, and limits on concurrent checkouts per account.
Black Friday is 5× normal.
Catalogue and read model scale with caches and replicas; the cart store scales horizontally; checkout is still only hundreds per second, but hot SKUs need the flash-sale treatment. Load-test the reservation path per SKU, not just in total.
Show "only 3 left" honestly.
Show exact low counts from the read model, accepting seconds of staleness; checkout's reservation gives the truth.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Inventory database primary lost | Checkout errors for seconds | Failover alarms | Replicated with automatic failover; carts and browsing unaffected |
| Payment provider slow | Slow "Pay" | Latency p99 | Timeouts, idempotent retries, reservation timeout covers the wait |
| Workflow crash mid-checkout | Nothing, or a short delay | Workflow heartbeats | Resume from the last completed step; compensations |
| Event pipeline delayed | Stale "in stock" badges | Read-model lag | Checkout re-checks; badge degrades to "check at checkout" |
| Sweeper stops | Items look out of stock | Expired-but-held count | Reads treat expired holds as free; alarm on backlog |
| Cart store partition down | Some carts briefly unavailable | Error rate by partition | Replication; local cart copy in the app as fallback |
12. Wrap-up and the clock
Trade-offs we made:
- Reserve at checkout, not at add-to-cart, with a timeout we tune.
- Strong consistency only where money and stock are decided; seconds of staleness on product pages.
- A saga with authorise-then-capture instead of distributed transactions.
- Idempotency keys on every write that matters.
- A separate read model for availability.
What we'd watch: checkouts started vs completed · reservations expired before payment · reservation conflicts per SKU · saga steps compensated · read-model lag · oversell incidents (target zero).
At 10×: more cache and cart capacity; checkout still fits sharded databases; hot SKUs move to the flash-sale design.
A 60-minute interview
| 0–5 | Frame: scale, when stock is held, warehouses, payment |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns, API, checkout data flow |
| 15–25 | First version: cart, checkout, expiry |
| 25–50 | Deep dives: reservations (9.1), saga (9.2), idempotency (9.3) |
| 50–60 | Read model, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including guest carts and prices |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, API, data flow |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: price changes, failures mid-payment, hot SKUs |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Cart in a key-value store, inventory with a conditional decrement, an order table, payment integration. |
| Senior | Reservations with timeouts, checkout as a saga with compensations, idempotency keys, outbox events, a read model for availability. |
| Staff+ | Where to spend consistency and where to save it, the reservation timeout as a business trade-off, warehouse allocation and reconciliation, the hand-off to the flash-sale design for hot items. |
Common mistakes:
- Reserving stock at add-to-cart.
- Product pages querying the inventory database.
- A distributed transaction across services and the payment provider.
- Capturing payment before the order exists.
- No idempotency on "Pay".
- Storing prices in the cart.
13. Try it, check yourself, read more
Reservation timeout tuner. 60 units, 300 shoppers starting checkout over 60 minutes (more than the stock can serve). Some abandon; payers take 4 minutes to pay. Set the hold time and the abandonment rate, and see sales, shoppers turned away while stock was only held, and payers whose hold expired.
Green = sold, amber = held by open checkouts, light = free. A payer whose hold expired re-reserves if stock is free, otherwise fails at payment. A smooth flow model (5 shoppers a minute), illustrative numbers.
When should stock be reserved?
How do you avoid taking money for an order that never gets created?
Where should product pages get availability from?
A shopper taps "Pay" twice after a timeout. What prevents a second order?
| Resource | Why |
|---|---|
| Dynamo: Amazon's Highly Available Key-value Store (SOSP 2007) | Why Amazon chose availability for the shopping cart, and how divergent carts merge. |
| Saga pattern (microservices.io) | Orchestrated and choreographed sagas with compensations. |
| Making retries safe with idempotent APIs (Amazon Builders' Library) | Idempotency keys in practice. |
| Building resilient payment systems with idempotency (Shopify Engineering) | How a large store makes checkout retries safe. |
| CQRS (Martin Fowler) | Separate read models, and when not to use them. |