Cart and Checkout
Turn a mutable shopping cart into one durable order while protecting inventory, payment identity and price agreement under retries.
Turn a mutable shopping cart into one durable order while protecting inventory, payment identity and price agreement under retries. This worked design targets SDE-2; the scale figures below are exercise assumptions, not claims about a named company's implementation.
Clarify the prompt
| Ask the interviewer | Decision to record |
|---|---|
| Does adding to a cart reserve stock? | Assume only checkout creates a timed reservation. |
| Can prices change before purchase? | Require a quote version and explicit confirmation. |
| Are partial orders allowed? | Assume all lines reserve atomically within one warehouse. |
Requirements and scope
The core deliverable is: Turn a mutable shopping cart into one durable order while protecting inventory, payment identity and price agreement under retries. The first design implements the APIs below and the state constraints in the data model. Agree the latency, freshness, retention and failure policy before adding infrastructure. Every successful acknowledgment must have an explicit meaning: accepted intent, committed state or completed external work. These are separate milestones.
Capacity and bottleneck reasoning
Assume 10 million daily shoppers, 20 cart writes each and one million checkouts/day: about 2,315 cart writes/s and 12 checkouts/s average. A 100x promotion burst makes inventory hot keys and payment capacity more important than averages.
Keep average and peak demand separate. Add replication, indexes, retries and safety headroom to raw arithmetic before selecting capacity. Load-test the busiest ownership key and recovery traffic as well as uniform happy-path throughput.
APIs and data model
- PUT /carts/{id}/lines/{sku} {qty,version} -> updated cart
- POST /checkouts {cartId,cartVersion,quoteVersion,requestKey} -> order or 409
- GET /orders/{id} -> pending,confirmed,cancelled or requires-action
cart(cart_id PK,user_id,version); line(cart_id,sku,qty)
order(order_id PK,checkout_key UNIQUE,quote_version,status)
reservation(order_id,sku,qty,expires_at,state); stock(sku,on_hand,reserved)
payment(order_id UNIQUE,provider_key,status)Authorize resource ownership at the API boundary; do not trust IDs or state supplied by a client. Bind retry identity to the caller and payload for mutating operations. Reject an incompatible replay instead of returning an unrelated stored result.
Component relationships
Read the diagram
Follow the labeled relationship panels; they are separate directed paths, not one global timeline.
Step-by-step design
1. Establish the authority
Cart storage prioritizes usability; cart quantities are not promises of stock. At checkout snapshot the accepted lines, shipping and price version into an immutable order and bind the request key to its payload.
2. Make retries and concurrent work safe
Reserve every SKU through an authoritative conditional transaction. Lock in SKU order to avoid deadlocks. Persist reservation state and an outbox workflow event together, then authorize payment using one provider identity.
3. Scale and recover the critical path
Confirm only after payment and still-valid reservations agree. Expiry and confirmation compete through conditional state transitions. If payment succeeds after stock is irreversibly released, record compensation/refund work and expose a recoverable order state.
Worked request and concurrency example
Two buyers check out the final unit. Only one reservation can increment reserved while on_hand-reserved >= 1. The winner starts payment; the other sees a stock conflict without being charged. If the winner retries POST after losing the response, the checkout key resolves to the existing order. An expired hold cannot be revived merely by a late webhook: the workflow checks the current order and reservation generation before confirming.
Failure and recovery walkthrough
Payment acceptance is uncertain after a timeout. Keep the order pending, reconcile with the provider and prevent a second payment identity. Compensation itself can fail; persist refund state and an operator-visible recovery queue.
Read the diagram
This numbered trace follows one scenario; explain the durable evidence at each transition.
Deep dive: an order is a durable agreement, not the cart
In this original exercise, a cart asks for SKU A quantity 1 and SKU B quantity 2. Stock has A=1 and B=2. Prices, tax, currency, shipping and accepted line versions become an immutable order quote. A client changing the cart later cannot mutate an in-progress payment. Scope checkout identity by tenant/customer and compare a canonical quote fingerprint on retry.
| Record | Authority and transition |
|---|---|
| order O | accepted → reserving → payment_pending → confirmed, or compensation/repair path |
| reservation H per SKU | active → consumed or released; one winning terminal transition |
| payment P | requested → authorized/captured/failed, with unknown as an evidence state |
| expiry work | May release only its matching active reservation generation |
Reserve all local lines together
When both stock rows share a transaction, acquire them in sorted SKU order, validate available quantities and create the order's unique reservation lines in the same transaction. If B is unavailable, roll back A's reservation too. The transaction does not call a provider.
begin
lock stock rows for [A, B] in stable order
require stock.on_hand - stock.reserved >= requested for every line
create unique reservation(order_id, sku, quantity, state=active, generation)
increase reserved for every line
insert order workflow outbox event
commitFor each SKU, define available = on_hand − reserved. Reserving lowers available without changing on_hand. Consuming lowers both on_hand and reserved; releasing lowers reserved only. A negative available or a double release is an invariant violation. An audit history plus idempotent transitions lets you rebuild these totals.
Make expiry and confirmation one conflict decision
Suppose P is authorized near H's expiry. Under this policy, confirmation succeeds only while the authoritative reservation remains active and eligible at database time. A bounded payment-in-progress extension may be a separate explicit transition. A worker cannot extend a hold just because it started before expiry.
Read the diagram
Payment P times out near hold expiry → Expiry releases the active hold → Another order reserves the same stock → P later reports captured funds → Old order compensates; new hold remains intact
If confirmation wins the conditional terminal transition, expiry becomes a no-op. If expiry wins and another order reserves the unit, a late payment success cannot reclaim it. Persist compensation with a stable key: void an authorization or refund captured funds according to the actual provider state. Compensation may time out too; the UI shows pending resolution rather than a fabricated successful purchase.
Cross-shard reservations expose an intermediate state
If A and B are owned by independent shards, do not pretend the local transaction spans them. The coordinator records each leg's stable identity and progress. Reserve A, then B; if B definitively rejects, release A using A's reservation identity. If B's reply is lost, first resolve B rather than issuing a fresh reservation. The order is pending until every required leg agrees.
This saga allows temporary stock withholding. Put an overall deadline and bounded TTL/renewal policy on it. Only consume when all legs are eligible under the chosen protocol. If irreversible partial consumption is allowed, define partial fulfillment and compensation explicitly. Otherwise keep the atomic consumption boundary within one authority or introduce a protocol whose blocking/failure behavior is justified. Writing “saga” on two independent commits does not make them all-or-nothing.
| Boundary | Durable evidence | Recovery |
|---|---|---|
| A reserved; coordinator crashes | A has O/A identity | Query O/A and resume |
| B rejected | Recorded rejection | Release matching active O/A once |
| P timed out | Same P identity, unknown result | Query/retry P under provider contract |
| P captured after H released | Capture evidence plus current hold state | Refund/repair; never decrement a new holder's stock |
| Refund reply lost | Refund identity and unknown outcome | Reconcile; do not create a fresh refund |
Protect the scarce decision from browsing load
Assume 10,000 checkout attempts/s and a measured safe reservation capacity of 2,000/s, including a reserved budget for confirm/expire/repair. At least 8,000/s must be rejected, delayed under a declared queue policy, or prevented upstream. A cached stock display cannot grant a reservation. Per-customer limits, bounded waiting rooms and hot-SKU isolation reduce wasted contention, but the stock predicate still chooses the winner.
Replay and reconciliation consume capacity too. Track active reservations, expiry lag, unknown payment value/age, refund age, admission rejection and quote conflicts. Avoid deleting reservation evidence while late callbacks or replays remain possible. An operator repair records identity and reason; it does not edit stock totals without a movement.
Checkout assessment: prove conservation
Question: A=1 and B=0, but an order requests one of each in a local atomic transaction. May A remain reserved after the operation fails?
Show answer and explanation
Answer: No under this all-lines contract. Roll back every local reservation. Cross-shard partial reservation requires an explicit pending/compensation protocol instead.
Question: A duplicate expiry message arrives after H was consumed. Should it increment available stock?
Show answer and explanation
Answer: No. Release is allowed only from the matching active generation. A consumed or already released hold produces no second stock effect.
Question: A late successful charge arrives after the last unit belongs to O2. May O1 become confirmed anyway?
Show answer and explanation
Answer: No. Its reservation is no longer eligible. Persist the proper void/refund/repair path and retain both payment and ownership evidence.
Score one point for immutable quote identity, local all-line atomicity, stock equations, expiry/confirm arbitration and durable compensation. Then change B to another shard and identify the extra pending states and recovery owner.
Continue with Outbox, Inbox, Sagas and Reconciliation and Transactions and Isolation. Provider-specific retry behavior must follow its documented operation lifetime; see Stripe idempotent requests as one concrete API reference, not a universal provider guarantee.
Decisions and alternatives
| Decision | Trade-off |
|---|---|
| Alternative 1 | A single-warehouse transaction simplifies all-or-nothing stock reservation. Cross-warehouse orders need an explicit saga and a policy for partial allocation. |
| Alternative 2 | Long holds reduce checkout failure but invite hoarding. Short holds improve availability while increasing expiry races and the need for clear UI timers. |
Operational contract
Monitor reservation age, checkout conversion, stock conflicts, unknown payment age and compensation backlog. Limit reservations per user, protect prices against client edits and rehearse a provider outage during a last-unit race.
Choose alert thresholds from the agreed user-facing objective. Assign an owner for stuck work and a replay/repair procedure that preserves the original business identity. Bound queue depth, concurrency and recovery throughput so an outage does not produce an uncontrolled catch-up burst.
Foundation, senior and staff depth
- Foundation: Explain immutable quote, local all-line reservation and stock equations.
- Senior: Resolve expiry versus confirmation and late payment with stable identities.
- Staff: Compare cross-shard pending holds with a single-authority design and budget compensation recovery.
Practice before revealing the answer
Spend 45 minutes: 5 clarifying scope, 7 on capacity/APIs/state, 12 on the first architecture, 15 on the concurrency and failure examples, and 6 on trade-offs and operations. Draw your own design before comparing it with this chapter.
Question: Why is a distributed lock around checkout alone insufficient?
Show answer and explanation
Answer: A lease can expire while an old worker continues. Durable stock predicates, unique checkout/payment identities and versioned state transitions must enforce correctness even if the lock holder is stale.
Verification and rollout
Race expiry, confirmation and duplicate release for every hold generation. Check available = on_hand - reserved, all-line local rollback and no consumption of another order's hold. Exercise refund uncertainty and cross-shard partial reservation; canary admission below measured stock/provider limits.
Primary technical reference
Official documentation for the mechanism used in this exercise. This lesson is an original interview design, not a description of undisclosed company internals.
Related study
Before this design, review these mechanisms and identify which owns the critical state transition:
Use the Interview Playbook to adapt the depth and timing, then return to the question breakdown overview.
Shared foundation: Transactions and Isolation
Transactions and Isolation: Predict write skew, enforce a shared invariant and retry whole transactions safely. Work its failure history before extending this design.
Shared foundation: Outbox, Inbox, Sagas and Reconciliation
Outbox, Inbox, Sagas and Reconciliation: Recover publication gaps, duplicate deliveries and uncertain compensation using durable business state. Work its failure history before extending this design.