Capstone: design a scalable e-commerce checkout system end to end — inventory, cart, payment, order confirmation
The hardest part of checkout isn't charging the card — it's guaranteeing two shoppers can't both buy the last unit, and rolling back cleanly when a step fails.
LEARN BY DOINGPut your intuition to work3 steps
Capstone: design a scalable e-commerce checkout system end to end — inventory, cart, payment, order confirmation
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#What "End to End" Actually Means Here
By the time a shopper clicks "Place Order," at least four services need to agree, in the right order, without any of them lying to each other: Cart (what's in the order and at what price), Inventory (is there actually stock, and can we hold it for this specific order), Payment (can we actually collect the money — this is the whole of the previous lesson, reused here as a subsystem), and Order (the durable record that ties the other three together once everything succeeds). Getting any one of these right in isolation is easy; the system-design difficulty is entirely in what happens when step 3 fails after steps 1 and 2 already succeeded, and in what happens when two requests hit step 2 for the same item at the same instant.
#Service Boundaries and Consistency Requirements
Not every part of this system needs the same consistency guarantee, and pretending otherwise is the most common over-engineering mistake in checkout design:
| Concern | Consistency needed | Why |
|---|---|---|
| Product catalog / browse pages | Eventually consistent (cache, search index) | Read-heavy, stale-by-seconds price/description is harmless |
| Cart contents | Session-strong, per-user | Only the owner reads/writes it; no cross-user contention |
| Inventory decrement at checkout | Strongly consistent, atomic | This is the overselling boundary, the one place correctness cannot bend |
| Payment authorization | Strongly consistent, idempotent (see the previous lesson) | Money |
| Order confirmation, shipping, recommendations | Eventually consistent, event-driven | Downstream of a committed order; a few seconds of lag is fine |
This split is a form of CQRS at the system level: the browsing experience reads from fast, denormalized, eventually-consistent stores, while the narrow checkout path — cart, inventory, payment, order — reads and writes the strongly consistent source of truth. Trying to make the catalog page strongly consistent (so the displayed stock count is always exact) buys you nothing and costs you the caching layer that makes the catalog fast; trying to make inventory decrement eventually consistent is how you oversell.
#The Overselling Problem, Solved at the Database Layer
The classic race: two shoppers both see "1 in stock," both click buy within milliseconds of each other. Whichever service naively does read quantity, check greater than zero, write quantity minus one as three separate steps will let both through, because both reads happen before either write. The fix is to make the check-and-decrement a single atomic operation the database enforces, not something the application coordinates:
UPDATE inventorySET quantity = quantity - 1WHERE sku = 'X' AND quantity >= 1;
If this statement affects zero rows, the item was already sold out and this request loses the race — cleanly, with no lock held across a network round trip and no distributed coordination needed. This is a conditional (optimistic) update; the alternative is a pessimistic SELECT ... FOR UPDATE that holds a row lock for the duration of the transaction, which is simpler to reason about but serializes every checkout attempt for a popular SKU behind a single lock — fine at moderate volume, a bottleneck at flash-sale volume. At very high contention on a single hot SKU, some systems shard the stock count itself (e.g., 10 counters of 100 units each instead of 1 counter of 1000) to spread the lock contention, reconciling to a single number periodically.
#The Checkout Saga
A single ACID transaction can't span Cart, Inventory, Payment, and Order when Payment means calling an external gateway over the network — you can't roll back a card charge with a database ROLLBACK. The standard answer is a saga: a sequence of local transactions, each with a defined compensating action if a later step fails.
- Reserve inventory (the atomic conditional update above, or a soft reservation with a TTL if you want to allow a checkout window before final decrement).
- Price and tax calculation — apply promotions, compute the final charge amount.
- Charge payment — synchronous call to the Payment Service from the previous lesson, using the order id as the idempotency key so a client retry (double-clicking "Place Order") can never create a second charge or a second order.
- Confirm the order — only after payment authorization succeeds; write the durable Order record.
- Compensate on failure — if payment is declined or times out unrecoverably, the compensating action releases the inventory reservation (quantity + 1) and marks the order CANCELLED. If inventory reservation itself fails (step 1), the saga never proceeds to payment at all — fail fast, before touching money.
#The Full Flow: Synchronous Path, Async Fan-out, and the Race
#Idempotency at the Checkout Level
Everything the previous lesson said about idempotency keys applies again here, one layer up: the checkout request itself needs an idempotency key (typically derived from the cart id plus a client-generated token), so that a double-click on "Place Order," or a client retry after a slow response, cannot create two orders or charge the card twice — the orchestrator's first step, before touching inventory at all, is checking whether this checkout has already been started and, if so, returning the in-flight or completed saga's result instead of starting a second one.
#Reservation TTLs and Abandoned Carts
A hard decrement at "add to cart" time would let shoppers hoard inventory just by browsing. A hard decrement only at "payment confirmed" time reopens the overselling window during the payment call itself. The practical middle ground is a soft reservation with a TTL: the conditional decrement happens when checkout starts (not at add-to-cart), and if the saga doesn't reach a terminal state (CONFIRMED or CANCELLED) within a bounded window — a few minutes — a background sweeper force-releases the reservation and returns the unit to the pool, the same abandoned-session pattern from the chunked-upload lesson earlier in this module, applied to inventory instead of file chunks.
#Failure Modes and Their Compensations
| Failure point | Compensating action |
|---|---|
| Inventory reservation fails (sold out) | Fail the checkout immediately; never call Payment |
| Payment declined | Release the inventory reservation; mark order CANCELLED |
| Payment gateway timeout (ambiguous) | Resolve via status check (previous lesson) before compensating or confirming |
| Orchestrator crashes mid-saga | Saga state persisted per step; a recovery process resumes or compensates from the last durable checkpoint |
| Order confirmed but event publish fails | Outbox pattern — event is durable in the same transaction as the order write; a relay retries the publish independently |
| Shopper abandons checkout | TTL-based sweeper releases the reservation |
🧠 Check your understanding
0/3 · 0/3 answered1. Why does the conditional UPDATE (WHERE quantity >= 1) solve the overselling race without a distributed lock?
2. In the checkout saga, what happens if payment is declined after inventory was already reserved?
3. Why does the checkout request itself need its own idempotency key, separate from the payment idempotency key?