Design e-commerce checkout.
The complete senior-level answer — built from the same question definition the interview simulator probes, and scored against the same rubric it grades with.
Last updated · built from the live interview engine’s question definition and rubric
ON HAND
500
what the warehouse holds
RESERVED
38
promises, each on a timer
AVAILABLE
462
what checkout may sell
available is the only number a buyer may be promised
Two ways to read this. Night before the interview: the revision sheet plus the one-line checkpoint that closes each section. With an evening: read straight through — the instruments are optional depth, not required reading.
Before you read
The answer in four anchors
Use these as the mental checklist while you skim; the sections below unpack each one.
Contract
Never sell a unit twice
The contract, before any box: a unit is sold at most once; every charge maps to exactly one confirmed order. And every promise the system makes — reservation, auth — must expire.Scale read
Correctness, not scale
One number to keep: ~120 orders/sec average. Checkout is won on correctness and the drop — not on sharding. Keep the boring path simple and spend the complexity budget on the hot SKU.Core commit
Reserve → authorize → confirm
Reserve → authorize → confirm → capture-at-ship, with the reserve as one conditional UPDATE and the confirm as one ACID transaction. Async begins after the commit — never before.Deep dives
Every promise expires
Three dives, one theme: every promise checkout makes must be cheap to break — reservations expire, auths void, capture waits for the truck. The only absolute is the conditional UPDATE.01 · The brief · minute zero
What you are handed
Design the checkout path for a large e-commerce store — from “add to cart” through payment to a confirmed order. Think Amazon on a normal Tuesday, and the same system on Black Friday.
Scale: 10M orders/day, ~100 product views per order, carts that live for weeks — and flash drops where 500K buyers arrive for 10K units in the first minute.
Key challenges: never sell the same unit twice, never charge a buyer without an order, decide when inventory is reserved and for how long, orchestrate a payment that takes seconds against stock that moves in milliseconds, and survive the drop without turning it into an oversell machine.
Decide: when do you reserve — add-to-cart or place-order? And what happens when the payment finishes after the reservation expired?
The 60-second path
Claim, re-price, reserve, authorize, confirm — the synchronous spine of a purchase. Everything on it is built to be undone, except the confirm.
One database
Stock, reservations, orders, outbox — one ACID home, so the confirm is one transaction. available = on hand − reserved is the page’s only equation.
Edges you don’t control
The buyer (abandons ~70% of carts) and the PSP (takes seconds, sometimes minutes). The reservation TTL is the bridge across both.
browse (cached) → cart → place order · claim → re-price → reserve (TTL) → authorize → confirm → outbox → ship → capture
Three zones and one clock. Draw this spine in the first two minutes — the whole interview is about what happens when the clock (the TTL) and the spine disagree. The full diagram is in §06.
What is really being asked
This is not a scale question in disguise — 10M orders/day is only ~120 orders a second, and saying so out loud is your first senior signal. The real subject is promises under time pressure: checkout promises a unit to a buyer seconds before the money moves, stock is finite and physical, and payment is slow and allowed to fail. The design question is where that promise lives, how long it lasts, and who cleans up when it breaks.
Winning shape: a reservation with a TTL as the promise, an atomic conditional decrement so the last unit is promised exactly once, and payment orchestrated as authorize-now, capture-at-ship — so every step before shipment can be undone cheaply. Candidates fail in the gaps: the read-then-write stock check, the reservation that never expires, the payment that lands after the promise lapsed.
02 · Requirements · minutes 0–8
Four requirements, four constraints
Functional
- A customer builds a cart that survives devices and sessions; a guest cart merges into the account on sign-in.
- Placing an order re-prices the cart server-side and reserves stock with a TTL — a visible countdown, not a silent hold.
- Payment is authorized at checkout and captured at shipment; the order confirms only when reservation and authorization both hold.
- A customer can cancel before shipment — cancel voids the auth and returns the stock. (Returns after delivery are a different flow — cut it.)
Requirement two hides the interview’s central decision: when to reserve. Reserve at add-to-cart and the ~70% of carts that abandon hold your stock hostage; never reserve and the buyer pays for a unit that’s already gone. Reserve at place-order, with a TTL.
Non-functional
No oversell, stated as a contract: a unit is sold at most once, and every charge maps to exactly one confirmed order. Say it before you draw a box.
Two consistency worlds, split on purpose. Browse is cached and may lie a little — “in stock” a few seconds stale is fine. The reserve path is strongly consistent and must not. Never let the badge and the reservation share a read path.
Latency: ~2s p99 for place-order — the PSP auth dominates; everything you control fits in ~200ms. Browse wants ~100ms from cache. Knowing which leg you’re timing is the signal.
Survive the drop. A flash sale is 100× normal traffic pointed at one SKU. The design must degrade to a queue — fair, slow, correct — never to an oversell.
Scope cuts to state explicitly
PSP internals — auth, capture, ledgers, and retries are the payment-system question; here the PSP is a black box with three verbs. Also cut, out loud: search and recommendations, the promo engine’s insides (place the call, skip the engine), multi-warehouse allocation (name the split, cut the routing), returns after delivery, and bot defense beyond the waiting room.
The contract, before any box: a unit is sold at most once; every charge maps to exactly one confirmed order. And every promise the system makes — reservation, auth — must expire.
03 · Back-of-envelope · minutes 8–13
Five numbers — and what they excuse you from building
These are fixed engine reference figures, not a calculator. Each result follows from the arithmetic next to it.
| Quantity | Derivation | Result |
|---|---|---|
| Average order rate | 10M orders / 86,400s | ~120 TPS |
| Peak order rate | evening ~3× · sale events ~10× | ~350 / ~1.2K TPS |
| Browse : buy | ~100 views per order → catalog reads | ~50K+ QPS, cached |
| The drop | 500K buyers · 10K units · first minute | ~8K attempts/s, one SKU |
| Storage | orders ~2KB × 10M/day ≈ 20 GB/day · ~100M open carts × ~2KB | ~7 TB/yr + ~200 GB KV |
What the numbers mean — the conclusions, not the arithmetic
- ~120 writes/sec fits one well-run Postgres with years of headroom. Say it out loud — declining to shard is a decision, and it’s the decision that keeps reserve-and-confirm a single ACID transaction.
- The tail is the test: the drop is ~8K attempts/sec against one row — lock contention, not aggregate throughput. No amount of horizontal scaling fixes a queue that forms on one SKU.
- Reads outnumber writes ~100:1 and want opposite things: browse is a cache/CDN problem that may serve stale truth; the reserve path must never read stale truth. Split them, and let the badge lie.
- ~70% of carts abandon — most reservations come home on their own. The TTL is a business dial: longer comforts buyers mid-3DS, shorter frees stock. Fifteen minutes is a defensible default; defending it is the point.
One number to keep: ~120 orders/sec average. Checkout is won on correctness and the drop — not on sharding. Keep the boring path simple and spend the complexity budget on the hot SKU.
04 · Core entities · minutes 13–17
Four entities and one state machine
Cart
cart_id, owner (user or anonymous token), lines of {sku, qty, price_at_add}. Server-side KV, weeks-long TTL, merged into the account on sign-in. price_at_add is display only — the order re-prices. A cart holds no stock, ever.
InventoryItem
sku, on_hand, reserved — and available = on_hand − reserved, derived, never stored as its own writable column. on_hand moves on receiving, shipping, and cycle counts; reserved moves only through checkout. Two writers, one invariant, one conditional UPDATE.
Reservation
reservation_id, order_id, sku, qty, expires_at, status: HELD → CONVERTED / RELEASED / EXPIRED. The promise, made physical — it bridges the seconds between “buy” and “paid,” and the TTL bounds how long the system is allowed to be wrong.
Order
order_id, idempotency_key (unique), lines with locked prices, auth_id from the PSP, and the state machine below. The order row doubles as the saga log — recovering a crashed checkout means reading the order, not guessing.
The order state machine
step with ← → or click a state
Entry — PLACED
The idempotency claim is won and the order row is written with locked prices. A double-click from here on is the same order.
Exit
Stock reserved → RESERVED.
What candidates get wrong here
Creating the order without a unique idempotency claim — the classic double-click becomes two orders and, later, two charges.
Recurring follow-ups: “what if the payment succeeds after the reservation expired?” (§07, dive 2 — the twenty-minute payment) and “what if the shelf is empty at pick time?” (phantom inventory — CANCELLED with an apology is a policy, not a bug).
Four entities, one equation: available = on hand − reserved. The reservation is the promise, the TTL bounds it, and the order row is the saga log.
05 · API design · minutes 17–20
Four endpoints and one header that carries the interview
POST /v1/carts/:id/items
POST /v1/orders Idempotency-Key: <client-generated>
GET /v1/orders/:id → { state, expires_at, … }
POST /v1/orders/:id/cancel Placing an order sends a cart_id and a payment method — the server re-prices; the client’s total is decoration. The response carries expires_at so the UI can show the reservation countdown honestly. Identity comes from the auth token, never the body. Cancel is a POST, not a DELETE — it’s a state transition with side effects (void, restock), not a row removal. And the payment outcome arrives on the PSP webhook — the 3DS redirect is UX, not truth.
Idempotency-Key semantics — all four cases
Pick a case: what does the server do?
New key
Create the order once; store key → order_id (24h TTL).
Show all four cases as a table
| Case | Behavior |
|---|---|
| New key | Create the order once; store key → order_id (24h TTL). |
| Retry: same key + same cart | Return the same order — no second order, no second reservation. |
| Same key, request still in flight | Return a conflict (409) or block briefly — never a silent second order. |
| Same key, different cart | Reject as a validation error — the client has a bug; guessing buys the wrong basket. |
Scope keys per user and bind them to a cart version — a stale tab retrying an old checkout must not purchase today’s cart.
Payment webhooks — four decisions that keep the async edge honest:
- Verify the PSP signature — an unsigned “payment_succeeded” is a free-goods endpoint.
- Dedupe by event id — the PSP will send it twice; confirming twice must be a no-op.
- Webhook vs redirect is a race: whichever arrives first confirms, and both paths are idempotent.
- When in doubt, poll the PSP — for auth state, the PSP is the source of truth, not your last event.
Four endpoints, one header. The field the client actually needs back is expires_at — an honest countdown converts better than a silent hold — and the whole retry story rides on the key.
06 · High-level design · minutes 20–30
Ten boxes — deliberately
One diagram, left to right, with fewer boxes than you might expect — at ~120 orders/second the win is knowing what not to add. Play a place-order through the happy path, or click any component for what interviewers listen for there.
Every component as plain text (all inspector notes)
- Storefront — external
- Browses the cached catalog, holds the buyer’s session, and shows the reservation countdown from expires_at. Identity rides the auth token. Never trusts its own math — prices and stock are re-validated server-side. The payment result comes from the order state, not the redirect. Failure mode: Treating the 3DS redirect as payment truth — redirects get lost and forged; the PSP webhook and the order state decide.
- API Gateway — synchronous checkout path
- Authenticates, applies per-user rate limits, and — on drop days — runs the virtual waiting room that admits buyers at a rate checkout survives. Naming the waiting room as the flash-sale front door is a senior move: protect the SKU row before traffic ever reaches it. Failure mode: Letting browse traffic anywhere near the inventory row — the stock badge is served from cache, not from the row checkout depends on.
- Cart Service — synchronous checkout path
- Durable server-side carts (Redis / Dynamo-class KV): survive devices, merge guest → account on sign-in. Line prices are price_at_add — display only. The cart is a wishlist, not a quote — it holds no stock and locks no price. One sentence on merge policy is plenty. Failure mode: Carts in localStorage die with the tab; carts that hold stock strangle it. Both are listed misconceptions.
- Checkout Orchestrator — synchronous checkout path
- The conductor: wins the idempotency claim, re-prices, reserves, authorizes, confirms — in that order. Saga state lives on the order row, so any instance can pick up a crashed checkout. The order of operations is the answer: reserve before authorize (stock is scarcer than payment capacity), capture last (capture is the irreversible step). Failure mode: The separating probe: auth succeeded, then the confirm transaction failed. Answer: a recovery sweep voids orphaned auths — the buyer was never captured, because auth ≠ capture.
- Inventory Service — durable state
- Owns the stock math: one conditional UPDATE checks and decrements available in the same statement. Reservations carry TTLs; a sweeper returns expired holds. on_hand changes only on receiving, shipping, and cycle counts; checkout only ever moves reserved. Two writers, one invariant. Failure mode: Read-then-write stock checks — two buyers both see “1 left,” both pass, oversell. The check and the decrement must be one statement.
- PSP — external
- The external payment provider, treated as a black box with three verbs: authorize at checkout, capture at ship, void on cancel. (Designing its insides is the payment-system question.) Webhooks verified by signature and deduped by event id. Webhook vs redirect is a race — both idempotent, first one wins; poll on doubt. Failure mode: Capturing at checkout — card rules tie capture to shipment, refunds cost real money, and every pre-ship cancel becomes a refund instead of a free void.
- Postgres — durable state
- One relational database holds all four tables — which is exactly what keeps reserve-and-confirm a plain ACID transaction. Partition orders by month; archive old ones. At ~120 orders/sec a single well-run Postgres is a feature, not a shortcut. Say why you are NOT sharding — that’s the senior version. Failure mode: Sharding by reflex: split stock from orders and the one-transaction confirm becomes a distributed problem you created yourself.
- Outbox → Bus — after the commit
- The outbox row commits inside the order transaction; a relay publishes it to the bus after commit. Consumers dedupe by event id. No event exists for an order that never committed — that is the entire point of the pattern. Failure mode: Publish-then-write mints pick tickets for orders that don’t exist — the warehouse ships a ghost.
- Fulfillment / WMS — after the commit
- Picks against allocations; the ship scan triggers the capture and decrements on_hand. Money and goods go final together. Capture rides the ship event. If the auth aged out (~7 days) behind a warehouse backlog, re-auth before shipping — never ship uncaptured. Failure mode: A failed pick (empty shelf — phantom inventory) needs a stated policy: cancel the line, void or refund, restock decision, make-good. Not an exception to swallow.
- Reconciliation — after the commit
- Three matches, on a schedule: system stock vs physical counts (cycle counting), reservations vs orders (no stuck holds), PSP auths vs orders (no orphaned charges). Phantom inventory is normal retail physics, not a bug — the database models the shelf; the count corrects the model. Safety stock absorbs the drift. Failure mode: No auth sweep: a crashed confirm leaves money held on a buyer’s card for days. A nightly void pass makes that a non-event.
Whiteboard minimum
A passing diagram has all of these.
- Storefront client
- API gateway (auth + rate limits)
- Cart service — server-side, durable
- Checkout orchestrator (stateless)
- Inventory with reservations + TTL
- Orders DB (ACID)
- PSP integration (auth / capture / webhook)
- Outbox → event bus
Senior+ additions
Unprompted.
- Virtual waiting room at the edge
- Idempotency claim on place-order
- Capture-on-ship via fulfillment events
- Expiry sweeper for reservations
- Reconciliation: counts · auths · cycle counts
Required flows
- Client → gateway → checkout, never direct to services
- Re-price server-side before reserving
- Reserve stock before authorizing payment
- Check + decrement as one conditional UPDATE
- Confirm + convert reservation + outbox in one ACID txn
- Capture only on shipment
Forbidden flows — penalized on sight
- Read stock, then decrement in a second statement
- Reserving at add-to-cart, or holds without a TTL
- Capture at checkout
- A charge that can exist without an order (no orphan sweep)
- Publishing to the bus before the order commits
- Browse traffic touching the inventory row
The request path, narrated — this is your whiteboard script
- The gateway authenticates and rate-limits. On a normal day it’s a pass-through; on a drop day the waiting room admits buyers at a rate the SKU row survives.
- Checkout — stateless — wins the atomic idempotency claim. From here, a double-click and a timeout retry are the same order.
- The cart is re-priced server-side and the order locks its own prices. The cart was a wishlist; the order is the quote.
- One conditional UPDATE reserves stock with a TTL — the sufficiency check and the decrement are one statement, so the last unit can’t be promised twice.
- The PSP authorizes; capture waits for the ship scan. A decline keeps the reservation for a retry until the TTL ends.
- One ACID transaction confirms the order, converts the reservation, and writes the outbox — the async shell (fulfillment, email, reconciliation) starts only after that commit.
One Postgres holds stock, reservations, orders, and the outbox — which is exactly what keeps reserve-and-confirm a single ACID transaction. At ~120 orders/sec (peaks ~1.2K) that is comfortable for years: partition orders by month, archive the cold ones, put read replicas under order history. Sharding is the move you defend not making — split stock from orders and you hand yourself a distributed transaction nobody asked for.
Reserve → authorize → confirm → capture-at-ship, with the reserve as one conditional UPDATE and the confirm as one ACID transaction. Async begins after the commit — never before.
07 · Deep dives · minutes 30–45
The three that decide the interview
1 — The last unit
The probe
“Two buyers hit place-order for the last unit in the same millisecond. Walk me through why exactly one of them gets it.”
Your answer, in order
- Name the race first: “read available, check it, then decrement” is a TOCTOU bug — both reads see 1, both checks pass, both writes land, available is −1 and two confirmation emails go out. The diagnosis is most of the credit.
- The fix is one statement, not two: UPDATE inventory SET reserved = reserved + 1 WHERE sku = ? AND on_hand − reserved ≥ 1. The row lock makes the check and the decrement atomic; the loser’s UPDATE matches zero rows and the API returns a clean “sold out.”
- Note what you did NOT need: no distributed lock, no serializable isolation, no queue — a single-row conditional write in a boring relational database. Reach for heavier machinery only when one row runs hot (dive 3).
- And the loser hears “sold out” at reserve time — before typing card details. Failing early is a UX feature that falls out of the correctness fix.
Staff extra — the same statement is why reservations beat naive decrements: a reservation with a TTL self-heals when its owner disappears mid-checkout; a bare decrement plus a crash leaks a unit until a human notices.
2 — The twenty-minute payment
The probe
“Your reservation TTL is 15 minutes. A buyer’s 3DS verification takes 20. The auth succeeds — for stock you released five minutes ago. What now?”
Your answer, in order
- Prevent most of it: extend the TTL while payment is verifiably in flight — bounded, one extension, not forever. A buyer mid-3DS is not an abandoner, and the clock should know the difference.
- For the ones that still slip: at confirm time, re-run the conditional reserve. Stock still there — take it and confirm normally. Stock gone — void the auth: the buyer was never captured, so this is an apology, not a refund.
- This is the irreversibility ladder the whole design climbs: an auth voids for free, a capture takes a refund, shipped goods are gone. Sequence the saga so the cheap-to-undo steps happen while promises can still break.
- The mirror failure — auth succeeded, then the confirm transaction crashed — leaves an orphaned auth: money held, no order. A recovery sweep reads order state and voids unmatched auths; the ~7-day auth expiry is the backstop.
Staff extra — say “void, not refund” — interviewers listen for whether you know capture is the point of no return, and that capture-at-ship is what keeps every pre-ship failure cheap.
3 — The drop
The probe
“You’re selling 10K sneakers. 500K buyers arrive in the first minute — 8K place-order attempts a second, one SKU. Your beautiful conditional UPDATE is now a queue on one row lock. Go.”
Your answer, in order
- Diagnose before fixing: this is contention, not capacity. The database isn’t out of CPU — 8K transactions serializing on one row lock is physics, and replicas (which scale reads) change nothing.
- Move the fight off the row. A waiting room at the edge admits buyers at a rate the row survives — fair, honest queueing beats a spinning checkout. In front of the database, an atomic counter (Redis DECR) tells 490K people “sold out” in microseconds, without a row lock.
- Winners of the counter proceed to the real conditional UPDATE — Postgres stays the source of truth. Redis is a bouncer, not a ledger: when they disagree, Postgres wins and reconciliation re-syncs the counter.
- Alternatives to name and price: bucketing the count across rows (spreads the lock, risks false sold-outs on stragglers) or a strictly serialized per-SKU queue (perfectly fair, adds a hop). Choosing by SKU-hotness is the senior answer.
Staff extra — the browse side matters as much — 500K people refreshing the product page must hit the CDN, never the row. The badge count is allowed to lie for seconds; the counter is not.
Scenario player
The same design under fire. What breaks, what the naive build does, what the correct mechanism does.
critical
What breaks
The last unit. Two buyers hit place-order in the same millisecond; at most one may win it.
What the naive design does
“Read available; if it’s at least 1, decrement.” Both reads see 1, both pass, available lands at −1. Two confirmation emails, one shoebox.
The correct mechanism
- One conditional UPDATE — the check lives in the WHERE clause, so check + decrement are atomic under the row lock.
- The loser matches zero rows → a clean “sold out” at reserve time, before card entry.
- No distributed lock, no queue — a single-row write is enough until the row runs hot.
- Reconciliation (reservations vs orders) is the detection net for anything that slips.
Components involved
Rapid-fire
- Multi-warehouse: available-to-promise per fulfillment node, allocated at confirm — name the split, don’t design the routing.
- Backorder vs hard sold-out is a business policy, not an engineering accident — saying the word “policy” is the answer.
- “Only 3 left” is marketing served from cache; the reservation is the contract. They’re allowed to disagree for seconds.
Three dives, one theme: every promise checkout makes must be cheap to break — reservations expire, auths void, capture waits for the truck. The only absolute is the conditional UPDATE.
08 · Traps & misconceptions
Seven claims that fail this interview
Each claim below is wrong. Read the claim, decide why it fails — then open it. These mirror the misconception patterns the simulator probes.
01 “reserve the stock when the item goes into the cart”
Why it fails — About 70% of carts abandon. Reserve at add-to-cart and your best sellers spend the day held hostage by window shoppers — a self-inflicted sold-out.
Instead — Carts hold nothing. Reserve at place-order with a TTL and a visible countdown; the sweeper returns abandoned holds.
02 “check the stock, then decrement it”
Why it fails — Two concurrent checkouts both read 1, both pass the check, both write — available goes negative and the last unit ships twice. Classic TOCTOU.
Instead — One conditional UPDATE with the check in the WHERE clause. Zero rows matched = sold out, cleanly, at reserve time.
03 “capture the payment at checkout”
Why it fails — Pre-ship cancels become refunds — slow, costly, disputed — and card-network rules tie capture to shipment. Every failure after payment now moves real money back.
Instead — Authorize at checkout, capture on the ship scan. Every pre-ship exit stays a free void.
04 “charge whatever price the cart shows”
Why it fails — Carts live for weeks; prices and promos don’t. Charging price_at_add turns every sale event into a dispute generator.
Instead — The cart is a wishlist. Re-price server-side at place-order; the order locks its own prices.
05 “eventual consistency is fine — it’s just shopping”
Why it fails — For browse, yes. But a stale read on the reserve path is an oversell: the badge lying is marketing; the reservation lying is a broken contract.
Instead — Split the paths: browse from cache and CDN (stale-tolerant), reserve against the strongly consistent row.
06 “we’ll handle the flash sale by autoscaling”
Why it fails — The drop bottlenecks on one row lock. Autoscaling multiplies the clients of that lock — the queue gets longer, not faster.
Instead — A waiting room at the edge, an atomic counter in front of the row, the database as truth. Contention, not capacity.
07 “publish “order placed” to the bus, then write the order”
Why it fails — A crash between publish and commit mints a pick ticket for an order that doesn’t exist — the warehouse ships a ghost.
Instead — Transactional outbox: the event commits with the order; a relay publishes after commit; consumers dedupe.
Failure drills — would you catch it?
Four attack scenarios from the engine, tagged by severity. Commit to an answer before opening the fix.
critical Two buyers, one unit, the same millisecond. The design reads available, checks it in application code, then decrements.
The mechanism — Collapse check + decrement into one conditional UPDATE, condition in the WHERE clause. The loser matches zero rows and hears “sold out” before card entry. Application-level checks are suggestions; the row lock is the law.
critical Place-order times out; the client retries with a fresh idempotency key it generated “for the new attempt.” The buyer is charged twice.
The mechanism — The key is generated once per checkout intent and reused across retries — a client contract you state in the API docs. Server-side, bind keys to the cart version; reconciliation (auths vs orders) catches what slips.
critical The confirm transaction deadlocks and rolls back — after the PSP approved the auth. Money is held on the card; no order exists.
The mechanism — An orphaned-auth sweep: scan auths with no confirmed order and void them — free, because capture never happened. The ~7-day auth expiry is the backstop. This failure staying cheap is why capture waits for shipment.
high Nobody built the expiry sweeper. By 4pm, thousands of abandoned checkouts hold stock, and the storefront shows sold-out over a warehouse of inventory.
The mechanism — The sweeper is not optional — expires_at without enforcement is a comment, not a TTL. Sweep on a short cadence, return units to available, and alert on reservation-age outliers as stuck-saga detection.
09 · How this gets scored
The rubric, verbatim
Scored as: “Senior engineer on a commerce / checkout platform team.”
Must-haves
- Prevents oversell with an atomic conditional reserve — names the TOCTOU race in check-then-decrement and puts the check in the same statement as the write.
- Reserves at place-order (not add-to-cart) with a TTL, a sweeper, and an explicit abandonment argument (~70% of carts walk).
- Orchestrates payment as authorize-at-checkout, capture-at-ship — sequenced reserve → authorize → confirm so every pre-ship failure is a free void — with idempotent order placement on top.
- Does the arithmetic (10M/day ≈ 120 TPS), concludes one ACID database over reflex-sharding, and splits browse (cached, stale-OK) from the reserve path (strongly consistent).
Nice-to-haves
- A real flash-sale plan: waiting room at the edge, atomic counter fronting the row, database as truth — framed as contention, not capacity.
- Phantom-inventory awareness: cycle counts, safety stock, and a stated policy for the empty shelf at pick time.
- PSP webhook discipline: signature verification, event-id dedupe, the webhook-vs-redirect race, poll-on-doubt.
Red flags
- Read-then-write stock math anywhere on the reserve path — the oversell is not hypothetical; it’s two concurrent requests away.
- Capture at checkout, or any charge that can exist without a confirmed order (no orphan sweep).
- Reservations without expiry — every abandoned checkout leaks a unit until the storefront lies.
The verdict ladder
Depth-ladder explorer
Four topics, four levels each — verbatim from the depth rubric. Slide L1→L4 and watch the same answer upgrade.
probed in: requirements, high-level design, deep dive
L1 — surface
Mentions checking stock before selling, without a concurrency story
L2
Proposes locking or a transaction around the stock check and decrement
L3
Makes check + decrement one conditional UPDATE (condition in the WHERE clause), names the TOCTOU race, and keeps available = on_hand − reserved as a derived value
L4 — staff+
Chooses between row locks, atomic counters, and per-SKU serialization by contention profile, and prices false sold-outs against oversell when the mechanisms trade off
This question rewards engineers who treat checkout as promises under time pressure, not CRUD. A Strong Hire reserves atomically, expires every promise, sequences payment so failures stay cheap, and does the arithmetic that keeps the design boring. A Lean Hire has the shapes but no clocks — reservations that never expire, auths that never get voided. A No Hire reads stock and writes it back. Weight correctness-under-concurrency and judgment-about-scale most heavily; the candidate who declines to shard 120 TPS has read the numbers, and the one who queues the drop has run one.
10 · FAQ
Common questions
- How hard is the e-commerce checkout interview question?
- Medium — the concepts (reservations, TTLs, auth vs capture, one conditional UPDATE) are each simple; the difficulty is sequencing them so every failure lands in a cheap-to-undo state. It’s a favorite senior-level screen because the arithmetic — 10M orders/day is only ~120 per second — quietly tests whether you design for real numbers or imagined scale.
- Should inventory be reserved at add-to-cart or at checkout?
- At place-order (or checkout start), with a TTL — never at add-to-cart. Roughly 70% of carts abandon; reserving on add turns browsing into a denial-of-service against your own stock. The reservation carries a visible countdown, and a sweeper returns expired holds to available.
- What is the single most common mistake?
- Reading the stock level, checking it in application code, and decrementing in a second statement. Two concurrent buyers both pass the check and the last unit sells twice. The fix is one conditional UPDATE with the sufficiency check in the WHERE clause — the check and the decrement become atomic.
- Do I need to shard the database?
- No — and saying so is a signal, not a cop-out. 10M orders/day is ~120 writes per second, comfortable for one well-run Postgres, and keeping stock, reservations, orders, and the outbox together is what makes the confirm a single ACID transaction. The hot-SKU problem is contention on one row — sharding doesn’t fix that either.
- How should I budget the 45 minutes?
- Roughly: 8 min requirements (say the contract), 5 estimation (do the division out loud), 7 entities + API (the reservation and the idempotency key), 10 high-level design (reserve → authorize → confirm → capture-at-ship), 15 deep dives — the last unit, the TTL-vs-payment race, and the drop are where hire / no-hire separates.
You’ve read the answer. Could you defend it?
Every candidate who fails this question has read a page like this one. The interview isn’t recall — it’s 45 minutes of follow-ups against your specific design, live. The simulator asks this exact question, probes the same oversell and TTL seams, and scores you on the same rubric — then tells you, honestly, where you actually stand.
Free 15-min session · No card · AI interviewer, honest verdict