Design an E-Commerce Shopping Cart and Order Flow
Learning Objectives#
- Learn the distinction that defines e-commerce: the cart is a session artifact, the order is a transaction, and conflating them is how systems oversell.
- Design the order placement as the single moment where stock is checked and committed, not the cart, where items can sit for days.
- Model the order as a state machine and see where idempotency, the guard against double-submission, belongs in the flow.
Introduction#
The shopping cart looks like a trivial case study: add things to a list, check out, done. The reality is that the cart is the single most mis-modeled object in e-commerce design, and the mistake is always the same one. Candidates put stock on the cart. The cart becomes the thing that knows how many units of an item are left, and the order becomes a copy of the cart. That design works until it does not: a cart can sit for a week while stock drains, and the moment of truth, whether the order can actually be filled, happens at checkout, not at add-to-cart. The cart is a wish list with prices frozen in time. The order is a transaction against current reality. The interview is whether you can keep those two apart, and place the one real concurrency checkpoint exactly where it belongs.
Requirements Gathering#
Functional requirements:
- A user adds items to a cart, changes quantities, and removes items, with the cart persisting across sessions.
- The cart shows line totals and a grand total based on the prices at the time of the calculation.
- Placing an order checks current stock for every line, reserves the units, records the total, and transitions through a defined order lifecycle.
- A user can see their past orders and each order's status.
- A cart item that becomes unavailable is flagged at checkout, not silently dropped.
Non-functional requirements:
- Order placement must be atomic: either every line is filled or none is, and the stock decrement and the order record happen together.
- Double-submitting a checkout must not create two orders.
Assumptions to state out loud: no promotions or coupon codes, no gift wrapping or shipping-cost modeling beyond a flat rate, no multiple currencies, and cart items hold a snapshot of the unit price. Cut promotions, cut shipping zones. The interviewer wants the cart-versus-order split and the checkout transaction, and both are clean only without a pricing engine on top.
Identifying Core Entities#
The entity list is where the case study is won, because the split the whole design depends on is visible in the list.
| Entity | One-line responsibility |
|---|---|
Cart | A session-scoped aggregation of CartItems, with quantities and computed totals. |
CartItem | A product, a quantity, and a frozen unit price snapshot. |
Product | The catalog entry with current stock and current price. |
Order | The transaction record: user, lines, total, status, and timestamps. |
OrderLine | A product, a quantity, and the price that was actually charged. |
InventoryService | The stock authority that decrements units atomically. |
OrderService | The checkout flow: validate, reserve, charge, confirm. |
Notice the two services: InventoryService owns stock, OrderService owns the flow. The cart owns neither. A Cart with an InventoryService reference is a cart that has grown a responsibility it should not have.
Class Design#
CartItem holds a price snapshot. The snapshot is the design decision in miniature: the price shown in the cart is the price at the time the item was added or last viewed, and the order charges a separately captured price, so both can differ from the catalog price without either being "wrong."
Cart is a plain aggregation with a total, deliberately boring. The absence of stock logic in here is the point.
Product carries current price and current stock, owned by the catalog side of the system. It is the thing the checkout checks against, and the thing the cart deliberately does not touch.
InventoryService is the stock authority, and its reserve is the atomic check-and-apply from the inventory chapter, wearing a reservation hat. The reservation is a separate concept from a hard decrement: it holds stock for the brief payment window, then either converts to a decrement on success or returns on failure. If you have read the movie booking chapter, this is the seat hold again, renamed.
Order is the state machine and the transaction record. The transitions are the guard rails: an order is PENDING at creation, CONFIRMED after payment, and the idempotency key lives alongside it.
OrderService is the checkout flow, and the order of operations is the entire design: build the order, reserve stock, then charge. The reserve is what makes the flow atomic. Two checkouts of the last unit, only one reserve succeeds, and the loser's order is marked FAILED and its stock was never touched.
Diagram: the cart-versus-order split, and the checkout flow where stock is committed exactly once — at reserve time, never at add-to-cart.
The idempotency check at the top is the double-submission guard: the same checkout retried by a flaky network returns the existing order instead of charging twice. The order of the guard, before the stock check, is what makes retries safe.
Design Patterns Used#
The honest pattern answer here is a modest Facade in OrderService, and the real structural idea is the transactional boundary: the checkout is a saga in miniature, reserve then charge, with the reservation as the compensating action if the charge fails. That is worth naming, because it is the pattern real e-commerce actually uses, and it is why the reservation is a separate step from a hard decrement. Do not reach for a Builder for the order (a constructor is fine), do not add a Strategy for shipping (cut from scope), and do not put an Observer on the cart to watch stock. The one place a Strategy could earn its keep is payment, which belongs to its own chapter later in this section.
Handling Edge Cases / Concurrency#
The concurrency story is the double-submission and the last-unit race, and both have homes in this design. The last-unit race: two users each add the last unit to their carts, both place orders, and reserve is the single choke point. The first reserve decrements, the second sees zero and returns false, and the second order is FAILED with its stock untouched. That is the same atomic check-and-apply as the movie seat, and the walkthrough is identical.
The double-submission race: a user double-clicks checkout or a network retry sends the same request twice. Without the idempotency key, both requests would reserve twice and charge twice. With it, the second request returns the existing order. The key is generated by the client or a gateway at the first request and reused for retries, and the store's putIfAbsent behavior is what makes concurrent duplicates collapse to one order.
The edge beyond the races: a cart item whose stock dropped below the cart quantity since the item was added. The checkout fails the reserve and the user sees a flagged line, which is the requirement that unavailable items surface at checkout rather than silently vanishing. And the cart total versus order total: the cart shows snapshot prices, the order charges snapshot prices too, and if the catalog price changed in between, neither is wrong, because the cart's snapshot was the quote the user saw. State that, and the "which price is correct" follow-up answers itself.
Common Mistakes#
The most common mistake is stock on the cart. CartItem.isAvailable or a cart-level stock check, so the cart decides whether the checkout can proceed. That design either checks stock at add time, which goes stale by checkout, or checks at checkout, which means the cart has grown an inventory service reference and the responsibility split is already gone. Stock lives in one place, InventoryService, and the checkout asks it, once.
The second mistake is a cart that becomes the order. The candidate models one object, the cart, and adds a status field to it. The order is not the cart with a flag, because the order is the transaction: it has its own lines, its own prices, its own idempotency, and its own lifecycle. Merging them means a user who edits their cart after placing an order has edited the order.
The third mistake is no idempotency. A double-click checkout, which happens on every single bad-network checkout in production, creates two orders and two charges. The candidate who says "we just check if the button was clicked twice" has an answer for the UI, not for the network. The idempotency key is the only honest guard.
Interview Perspective#
A weak answer is Cart with a checkout() method that decrements product stock directly. The interviewer asks "what if the user's cart is a week old" and the answer is silent, because the cart had no snapshot and the stock check, if it exists, ran at add time. The order is a copy of the cart and there is no transaction boundary anywhere.
A strong answer says "the cart is a session artifact with price snapshots, the order is a transaction, and checkout is reserve, then charge, with an idempotency key at the front." Follow-ups to expect: "what if the price changed between cart and order" (the cart showed a snapshot, the order charges the snapshot, and a price-change banner is a UI concern, which is the honest scope line), "what if stock is reserved but the payment fails" (the reservation is released or converted, which is where the saga's compensating step lives, and it belongs to the payment chapter), "what if the user removes an item after checkout" (the order lines are immutable, the cart is a different object, which is exactly why they are separate). The strongest candidates volunteer the last-unit race and the idempotency key unprompted, because they have seen both in production.
Knowledge Check#
- A user adds the last unit of an item to their cart, waits three days, and checks out. During those days, no stock arrived. Trace the checkout and state which object decides the outcome, and why the cart's earlier stock check, if it had one, could not have decided it correctly.
- A flaky network resubmits the same checkout request twice concurrently. Walk through
placeOrderfor both requests and explain what each one returns and how many orders exist afterward. - The cart's snapshot price is 500 cents, and the catalog price rose to 700 cents before checkout. Which price does the order charge, and why is that defensible rather than a bug?
Key Takeaways#
- The cart is a session artifact with price snapshots. The order is a transaction. Never merge them.
- Stock lives in one place, and the checkout asks it once, at reserve time.
- Checkout is reserve, then charge, with the reservation as the compensating step. That is the transaction boundary.
- The idempotency key is the guard against double-submission, and it runs before anything else mutates.
- The last-unit race resolves at the reserve, and the loser's order fails cleanly with no stock touched.
What's Next#
The order flow introduced the reserve-then-charge boundary and the idempotency guard. The notification system keeps the async handoff but changes the product: the message is no longer a stock unit, it is a fact, and the design problem is orchestrating channels, templates, and the queue that keeps millions of them from flattening a mail server.