InterviewKata
System design · Worked design D36 of 46 · Location and marketplaces · Long form

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:

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

  1. A shopper can add items to a cart, as a guest or logged in, from any device, and the cart is still there tomorrow.
  2. A shopper can check out: stock is reserved, payment taken, an order created, exactly once.
  3. 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

TargetWhy this number
Never oversell a non-backorder item consistencyCancelling a paid order is the worst customer experience in retail.
Cart writes always succeed (99.99%), even during partial outages surviving failuresA 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 safetyRetries and timeouts happen on every flaky phone network.
Product pages: availability no more than ~10 s stale; pages under 200 ms p99 speedFast browsing sells; a slightly stale "in stock" is acceptable because checkout re-checks.
Holiday peaks of 5× without degradation load shapeThe peak days are the year's revenue.

5. Numbers that change the design

QuantityWorking→ so we…
Product page views50 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 writesMaybe 5 per visitor ≈ 250 M a day ≈ 3,000/s, 15,000/s at peakSimple key-value writes → so carts live in a partitioned key-value store keyed by cart id
Checkouts1 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

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.

Parked for laterthe right hold time (9.1) · a step fails halfway (9.2) · retries and double orders (9.3) · availability for millions of page views (9.4) · carts that must always accept writes (9.5) · multi-warehouse allocation (9.6).

9. Make it hold up

9.1 When to hold stock, and for how long never oversell

ApproachWhy
✗ BadRead the count, then decrement at payment, with no conditionTwo buyers read "1 left" and both pay: an oversell.
~ GoodConditional decrement at payment time onlyNever oversells, but a shopper fills in a form for five minutes and is told at the end that it's gone.
✓ GreatReserve 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 expiryNo 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

ApproachWhy
✗ BadOne distributed transaction across inventory, payment and ordersTwo-phase commit across services and an external payment provider: slow, fragile, often impossible.
~ GoodA sequence of calls from the checkout service, hoping each succeedsA crash between "payment captured" and "order created" leaves money taken with no order.
✓ GreatAn 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

ApproachWhy
✗ BadQuery stock for every page viewProduct pages hammer the database that checkout depends on.
~ GoodCache counts with a TTLCheap, but popular items flip between in and out of stock unpredictably.
✓ GreatA 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 cacheProduct 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

FailureWhat users seeHow we detect itWhat limits the damage
Inventory database primary lostCheckout errors for secondsFailover alarmsReplicated with automatic failover; carts and browsing unaffected
Payment provider slowSlow "Pay"Latency p99Timeouts, idempotent retries, reservation timeout covers the wait
Workflow crash mid-checkoutNothing, or a short delayWorkflow heartbeatsResume from the last completed step; compensations
Event pipeline delayedStale "in stock" badgesRead-model lagCheckout re-checks; badge degrades to "check at checkout"
Sweeper stopsItems look out of stockExpired-but-held countReads treat expired holds as free; alarm on backlog
Cart store partition downSome carts briefly unavailableError rate by partitionReplication; local cart copy in the app as fallback

12. Wrap-up and the clock

Trade-offs we made:

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–5Frame: scale, when stock is held, warehouses, payment
5–10Must do, how well, numbers
10–15Nouns, API, checkout data flow
15–25First version: cart, checkout, expiry
25–50Deep dives: reservations (9.1), saga (9.2), idempotency (9.3)
50–60Read model, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including guest carts and prices
8–15Must do, how well, numbers
15–22Nouns, API, data flow
22–35First working version
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: price changes, failures mid-payment, hot SKUs
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelCart in a key-value store, inventory with a conditional decrement, an order table, payment integration.
SeniorReservations 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:

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?

Most carts are abandoned; holding stock for them hides it from real buyers.

How do you avoid taking money for an order that never gets created?

No money moves until the order exists; a void undoes the authorisation.

Where should product pages get availability from?

Seconds of staleness is fine; checkout's reservation is the real check.

A shopper taps "Pay" twice after a timeout. What prevents a second order?

Plus a unique checkout id on orders, as a database-level guard.
ResourceWhy
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.