Skip to main content

Design an E-commerce Checkout System

Checkout is the one part of an e-commerce system where "eventually consistent and probably fine" isn't good enough — selling the same last unit of inventory to two different customers, or charging a card twice for one order, are the kind of correctness failures that show up as real customer complaints and real financial loss, not a briefly stale page. Following the framework from How to Answer a System Design Interview Question.

1. Requirements​

Functional:

  • A customer adds items to a cart, then checks out: inventory is reserved, payment is charged, and an order is created.
  • If payment fails, any reserved inventory is released back to available stock.

Non-functional:

  • Inventory decrements must be strongly consistent — this is squarely a case where Strong vs. Eventual Consistency comes down firmly on the strong side: two customers must never both successfully buy the last unit of a product.
  • Payment must be processed exactly-once from the customer's perspective, even if the network fails and the client retries the checkout request.
  • The overall checkout flow spans multiple services (inventory, payment, order, notification) that must stay consistent with each other without one giant cross-service transaction.

Scale estimate: assume 50,000 checkouts/minute at peak (a large sale event), with a much smaller fraction contending for the same specific, limited-stock item — the general checkout path is comfortably scalable, and the interesting correctness problem is concentrated in that hot, contended case.

2. API Design​

POST /checkout
body: { cart_id, payment_token }
returns: { order_id, status: pending|confirmed|failed }

GET /orders/{order_id}
returns: { status, items, total }

3. Data Model​

inventory
product_id VARCHAR PRIMARY KEY
available_qty INT
reserved_qty INT

orders
order_id VARCHAR PRIMARY KEY
cart_id VARCHAR
status ENUM(pending, confirmed, failed)
idempotency_key VARCHAR UNIQUE

Splitting available_qty from reserved_qty is the concrete mechanism behind the whole design: reserving stock during checkout decrements available_qty immediately (so no other customer can claim it), while reserved_qty tracks stock that's held but not yet confirmed by a successful payment — released back to available_qty if payment fails.

4. High-Level Design​

System Design Lab

Each step is its own service with its own database, following Microservices Architecture — which is exactly what makes "one ACID transaction across all of them" impossible, and why the flow is instead a sequence of individually-committed steps, each able to compensate (release the reservation) if a later step fails.

5. Deep Dive: exactly-once payment under retries​

If a client's checkout request times out waiting for a response, it doesn't know whether the payment actually succeeded — and a naive retry risks charging the customer twice, exactly the scenario Idempotency exists to prevent. The client sends the same idempotency_key (generated once per checkout attempt, reused across retries of that same attempt) on every retry; the payment service checks whether it's already processed that key and, if so, returns the original result instead of charging the card again. The orders table's UNIQUE constraint on idempotency_key enforces this at the database level as a hard guarantee, not just an application-logic convention that could be bypassed by a bug.

The inventory reservation has its own version of the same underlying problem, solved differently: decrementing available_qty needs to be an atomic, conditional operation (UPDATE inventory SET available_qty = available_qty - 1 WHERE product_id = ? AND available_qty > 0) that fails cleanly if stock hits zero, rather than a read-then-write pair that two concurrent checkouts could race on — the same class of correctness problem Design a Parking Lot System solves with row-level locking for exactly the same reason: only one of two concurrent claims on the same scarce resource can win.

6. Deep Dive: recovering when a later step fails after payment already succeeded​

The happy path, and the payment-fails case above, both leave the system in a clean state. The harder case is the reverse: payment succeeds, but a later step — creating the order record, say, because of a transient database outage — fails afterward. There's now a real customer who has been charged with no order to show for it, and because Microservices Architecture already ruled out one ACID transaction spanning all these services, there's no database rollback that can undo the charge.

The fix is the Saga pattern already named in Further Reading, made concrete: every step in the flow that changes external state defines its own explicit compensating action — reserve has release, charge has refund — and when any step fails, the services already completed run their compensations in reverse order, undoing the flow's effects one step at a time instead of relying on a rollback that doesn't exist across service boundaries.

System Design Lab

Each compensating action needs exactly the same correctness guarantee as the forward action it undoes: a retried refund must not double-refund, which is the same idempotency_key mechanism from the deep dive above, just applied to the undo path instead of the original charge.

7. Tradeoffs​

Reserving inventory before payment succeeds (rather than only decrementing stock after a confirmed charge) means a customer whose payment ultimately fails briefly held stock they didn't buy — a small, temporary cost. The alternative — charging first, then checking stock — risks successfully charging a customer for an item that's no longer available, a strictly worse failure to explain to a customer than a declined payment.

The Saga-based recovery above trades a stronger guarantee (one atomic all-or-nothing transaction) for one that's honest about what a microservices architecture can actually offer: the overall order is only eventually consistent across all three services, even though each individual service's own state (inventory's stock count, payment's charge record) stays strongly consistent on its own. That gap matters in practice — a refund compensation can be issued instantly by this system but take several business days to actually appear on a customer's statement, a real-world settlement delay this design can trigger correctly but can't make disappear.

Reserve-then-charge vs. charge-then-reserve: pros and cons​

Reserve-then-charge

  • Never charges a customer for an item that turns out to be unavailable
  • A failed payment cleanly releases the reservation with no financial cleanup needed
  • Matches the intuitive checkout flow: claim it, then pay for it

Charge-then-reserve

  • Can charge a customer successfully only to discover the item is no longer in stock
  • Requires an explicit refund flow for a class of failure that reserve-then-charge avoids entirely
  • Creates a worse customer experience than a simple declined-payment message

Further Reading​

Share this lesson

Saved locally in your browser — visible in the sidebar as you go.