Amazon Online Shopping System
Catalog, cart, orders, payments, shipping. The biggest single OOD prompt; the discipline is *what you cut*.
Context#
Amazon-scale online shopping is the largest OOD prompt by surface area: catalog, search, cart, checkout, payments, fulfilment, shipping, returns, reviews, recommendations, seller onboarding, multi-warehouse inventory, fraud, customer support. Every one of those is its own round. The LLD discipline on this prompt is not how much you can cover — it is how decisively you cut. An interviewer scoring this round watches you fight the urge to draw the whole map.
The HLD overlap is real: search-at-scale, recommendations, feed-fan-out for product pages, cross-region inventory consistency are system-design problems with system-design answers (sharding strategy, async pipelines, eventual consistency). An LLD round is not the place to design them. The right move is to name the seam — “Catalog is the read API; the implementation is out of scope here” — and move on.
The interviewer’s hidden objectives, in order:
- Can you declare scope first and stick to it? Bonus points for naming what you cut.
- Can you identify the five core aggregates — Product/Catalog, Cart, Order, Payment, Shipment — and keep their state machines independent?
- Can you draw the Order lifecycle crisply (it is the biggest single artefact in this round)?
- Can you apply the Command pattern for checkout (so each step is replayable / cancellable) and the State pattern for Order?
- Can you handle payment failure mid-checkout and out-of-stock at fulfilment without hand-waving?
- Can you defend a list of explicit cuts at the end of the round?
Requirements (functional and non-functional)#
Scope is the most points-bearing decision in this round. Below is a defensible cut that fits 45 minutes.
Functional — in scope.
- A catalog of products. Each product has an id, title, price, and a stock count maintained at the SKU level. Browsing returns products; the search implementation is out-of-scope (a
CatalogServiceinterface marks the seam). - A customer can add to cart, update quantity, and remove. The cart belongs to a customer and persists across sessions.
- Checkout turns a cart into an order: it locks stock, captures payment, creates a shipment, clears the cart. Each step is independently logged.
- An Order walks the lifecycle
Placed → Paid → Picked → Packed → Shipped → Delivered, withCancelledreachable fromPlaced/PaidandReturnedreachable fromDelivered. - A customer has multiple Addresses; one is selected per order.
- Payment is via CreditCard / UPI / GiftCard — a strategy interface.
Functional — out of scope (called out explicitly, and decisively).
- Search and ranking. A separate HLD round.
CatalogService.search(query)is a seam. - Recommendations and personalisation. HLD round. Same seam shape.
- Reviews and ratings. Their own aggregate; named but not designed.
- Seller onboarding, listings, payouts. A whole second platform.
- Multi-warehouse inventory routing. Inventory is one number per SKU here; cross-DC routing is a fulfilment service round.
- Returns policy mechanics. The
Returnedstate exists; the refund window / RMA flow is a separate problem. - Promotions, coupons, dynamic pricing. Strategy seam on
PricingPolicy; concrete implementations skipped. - Fraud detection. A
FraudCheckseam in checkout; implementation out of scope. - Notifications. Observer hook on
Orderstate changes; the channel implementations skipped.
Stating these cuts on the whiteboard first is the move that separates a strong round from a meandering one.
Non-functional.
- The model should support 10⁷ products and 10⁶ daily orders in principle, but the OOD round optimises only the in-process shape — repositories are seams, not implementations.
- Checkout must be atomic across stock lock + payment + shipment creation, with compensation on any failure.
- Order state changes are observable (audit + notifications hook).
- Concurrency: two customers buying the last unit must not both succeed.
Use case diagram#
┌────────────────┐ │ Customer │ └────────┬───────┘ │ ┌────────────────┼────────────────┬────────────────┐ ▼ ▼ ▼ ▼ [browse catalog] [manage cart] [checkout] [track / return] │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ Online Shopping System │ └─────┬───────────────┬─────────────────┬─────────────────┘ ▲ ▲ ▲ │ │ │ ┌───────────┐ ┌──────────────┐ ┌────────────┐ │ Seller │ │ Fulfilment │ │ Support │ ├───────────┤ ├──────────────┤ ├────────────┤ │ (cut) │ │ pick / pack │ │ refund │ │ │ │ ship │ │ cancel │ └───────────┘ └──────────────┘ └────────────┘Four actors. Seller flows are explicitly cut. Fulfilment is internal (the warehouse worker driving an Order from Paid to Shipped). Support owns cancellation and refund.
Class diagram#
┌────────────────────────┐ ┌────────────────────────┐ │ Catalog │ │ Customer │ ├────────────────────────┤ ├────────────────────────┤ │ findById(id) │ │ id, name, email │ │ search(query) // seam │ │ addresses : List │ └──────────┬─────────────┘ │ paymentMethods : List │ │ 1..* │ cart : Cart │ ▼ └────────────────────────┘ ┌────────────────────────┐ │ Product │ ├────────────────────────┤ ┌────────────────────────┐ │ id, title, price │ │ Cart │ │ stock │ ├────────────────────────┤ │ reserve(qty) │ │ items : Map<Product,Qty>│ │ release(qty) │ │ add / remove / update │ │ ship(qty) │ │ subtotal() │ └────────────────────────┘ └──────────┬─────────────┘ │ checkout() ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ Order │ │ CheckoutService │ ├────────────────────────┤ ├────────────────────────┤ │ id, customer, items │◀────────│ place(cart, address, │ │ status : OrderStatus │ │ payment) │ │ payment, shipment │ │ // Command sequence: │ │ total, address │ │ LockStock, │ │ place / cancel / ... │ │ CapturePayment, │ └────────┬───────────────┘ │ CreateShipment │ │ └────────────────────────┘ │ 1 ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ Shipment │ │ PaymentStrategy │◁── Card, UPI, ├────────────────────────┤ ├────────────────────────┤ GiftCard │ id, items, address │ │ pay(amount) : Receipt │ │ status : ShipState │ │ refund(receipt) │ │ tracking │ └────────────────────────┘ └────────────────────────┘Three patterns are doing the load-bearing work:
- State pattern on
Order.statusandShipment.status. They are linked but not coupled —Shipment.ShippedcausesOrder.Shippedvia an observer, not a direct call. - Command pattern on the checkout pipeline.
LockStock,CapturePayment,CreateShipmentare commands withexecute()+compensate(). The pipeline is the saga. - Strategy pattern on
PaymentStrategy, and (out-of-scope but seamed)PricingPolicy.
What is deliberately not in the diagram:
- No
OrderManagergod class. Checkout is a sequence of commands; once placed, the order owns its transitions. - No
Inventoryaggregate. Stock lives onProductfor this round. (Real Amazon would have a multi-warehouseInventoryService; that’s the cut.) - No
Promotion,Review,Recommendation. Cut.
Sequence diagram (key flows)#
Checkout, as a command saga:
Customer Cart Checkout Product Payment Shipment Order │ checkout │ │ │ │ │ │ │─────────►│ │ │ │ │ │ │ │ place(cart,address,pay) │ │ │ │ │ │───────────►│ │ │ │ │ │ │ │ LockStock.execute() │ │ │ │ │ │─────────────►│ │ │ │ │ │ │ reserve(qty) per item │ │ │ │ │ │ │ │ │ │ │ │ │ CapturePayment.execute() │ │ │ │ │ │─────────────────────────────►│ │ │ │ │ │ receipt │ │ │ │ │◄─────────────────────────────│ │ │ │ │ │ CreateShipment.execute() │ │ │ │ │ │────────────────────────────────────────────►│ │ │ │ │ shipmentId │ │ │ │ │◄────────────────────────────────────────────│ │ │ │ │ new Order(PLACED) ──────────────────────────────────────────►│ │ │ │ mark(PAID) │ │ │ │ │ │ clear() │ │ │ │ │ clear() │ │ │ │ │ │◄───────────│ │ │ │ │ Order │ │ │ │ │ │◄─────────│ │ │ │ │Failure path — payment declines after stock is locked:
Checkout Product Payment Shipment Order │ LockStock.execute() │ │ │ │───────────────────► │ │ │ │ CapturePayment.execute() │ │ │ ─────────────────────────────────► │ │ │ PaymentFailed │ │ │ ◄───────────────────────────────── │ │ │ LockStock.compensate() │ │ │───────────────────► │ │ │ │ release(qty) per item │ │ │ │ │ (no order persisted; checkout returns failure) │The saga is short — three commands — and the compensation is mechanical because each command knows how to undo itself.
Activity diagram (for non-trivial state)#
The Order lifecycle is the biggest single artefact in this round:
┌─────────┐ │ start │ └────┬────┘ ▼ ┌────────────┐ ┌───────►│ Placed │── payment captured ─────►┌────────────┐ │ └─────┬──────┘ │ Paid │ │ cancel │ └─────┬──────┘ │ (pre-pay) │ │ picked │ ▼ ▼ │ ┌────────────┐ ┌────────────┐ │ │ Cancelled │◄── cancel ───────────────│ Picked │ │ └────────────┘ └─────┬──────┘ │ │ packed │ ▼ │ ┌────────────┐ │ │ Packed │ │ └─────┬──────┘ │ │ shipped │ ▼ │ ┌────────────┐ │ │ Shipped │── delivered ──►┌──────────┐ │ └────────────┘ │Delivered │ │ └─────┬────┘ │ │ return │ ▼ │ ┌──────────┐ └─── (re-order — new Order) ─────────────────────────────────────────────────│ Returned │ └──────────┘Cancelled is reachable from Placed/Paid only — once picked, it becomes a Returned flow. Returned is terminal. Delivered → Returned requires the customer to initiate the return (out-of-scope mechanics; the state exists).
Java implementation#
A representative slice. The checkout saga, the order state machine, and the cart are shown; payment and shipment are sketched.
public enum OrderStatus { PLACED, PAID, PICKED, PACKED, SHIPPED, DELIVERED, CANCELLED, RETURNED }public enum ShipmentStatus { PENDING, IN_TRANSIT, DELIVERED, FAILED }
public final class Product { private final String id; private final String title; private final Money price; private int stock;
public Product(String id, String title, Money price, int stock) { this.id = id; this.title = title; this.price = price; this.stock = stock; }
public synchronized void reserve(int qty) { if (qty > stock) throw new OutOfStockException(id); stock -= qty; }
public synchronized void release(int qty) { stock += qty; }
public String id() { return id; } public Money price() { return price; }}
public final class Cart { private final String customerId; private final Map<Product, Integer> items = new LinkedHashMap<>();
public Cart(String customerId) { this.customerId = customerId; }
public void add(Product p, int qty) { items.merge(p, qty, Integer::sum); } public void remove(Product p) { items.remove(p); } public void update(Product p, int qty) { if (qty == 0) items.remove(p); else items.put(p, qty); } public void clear() { items.clear(); }
public Money subtotal() { return items.entrySet().stream() .map(e -> e.getKey().price().times(e.getValue())) .reduce(Money.zero(), Money::plus); }
public Map<Product, Integer> items() { return Collections.unmodifiableMap(items); }}
public interface CheckoutStep { void execute(CheckoutContext ctx); void compensate(CheckoutContext ctx);}
public final class CheckoutContext { public final Cart cart; public final Address address; public final PaymentStrategy payment; public Receipt receipt; public String shipmentId; public CheckoutContext(Cart c, Address a, PaymentStrategy p) { this.cart = c; this.address = a; this.payment = p; }}
public final class LockStockStep implements CheckoutStep { public void execute(CheckoutContext ctx) { // Order matters: rely on a deterministic key ordering to avoid deadlocks // when two checkouts touch overlapping products. ctx.cart.items().forEach((p, q) -> p.reserve(q)); } public void compensate(CheckoutContext ctx) { ctx.cart.items().forEach((p, q) -> p.release(q)); }}
public final class CapturePaymentStep implements CheckoutStep { public void execute(CheckoutContext ctx) { ctx.receipt = ctx.payment.pay(ctx.cart.subtotal()); } public void compensate(CheckoutContext ctx) { if (ctx.receipt != null) ctx.payment.refund(ctx.receipt); }}
public final class CreateShipmentStep implements CheckoutStep { private final ShipmentService shipping; public CreateShipmentStep(ShipmentService s) { this.shipping = s; } public void execute(CheckoutContext ctx) { ctx.shipmentId = shipping.create(ctx.cart.items(), ctx.address); } public void compensate(CheckoutContext ctx) { if (ctx.shipmentId != null) shipping.cancel(ctx.shipmentId); }}
public final class CheckoutService { private final List<CheckoutStep> pipeline; private final OrderRepository orders;
public CheckoutService(List<CheckoutStep> pipeline, OrderRepository orders) { this.pipeline = pipeline; this.orders = orders; }
public Order place(Customer c, Cart cart, Address addr, PaymentStrategy pay) { CheckoutContext ctx = new CheckoutContext(cart, addr, pay); List<CheckoutStep> executed = new ArrayList<>(); try { for (CheckoutStep s : pipeline) { s.execute(ctx); executed.add(s); } } catch (RuntimeException e) { Collections.reverse(executed); for (CheckoutStep s : executed) { try { s.compensate(ctx); } catch (RuntimeException suppressed) { /* log */ } } throw e; } Order o = new Order(UUID.randomUUID().toString(), c, cart.items(), addr, ctx.receipt, ctx.shipmentId, cart.subtotal()); o.markPaid(); orders.save(o); cart.clear(); return o; }}
public final class Order { private final String id; private final Customer customer; private final Map<Product, Integer> lines; private final Address shipTo; private final Receipt receipt; private final String shipmentId; private final Money total; private OrderStatus status = OrderStatus.PLACED;
public Order(String id, Customer c, Map<Product, Integer> lines, Address a, Receipt r, String shipmentId, Money total) { this.id = id; this.customer = c; this.lines = Map.copyOf(lines); this.shipTo = a; this.receipt = r; this.shipmentId = shipmentId; this.total = total; }
public void markPaid() { advance(OrderStatus.PLACED, OrderStatus.PAID); } public void markPicked() { advance(OrderStatus.PAID, OrderStatus.PICKED); } public void markPacked() { advance(OrderStatus.PICKED, OrderStatus.PACKED); } public void markShipped() { advance(OrderStatus.PACKED, OrderStatus.SHIPPED); } public void markDelivered() { advance(OrderStatus.SHIPPED, OrderStatus.DELIVERED); } public void markReturned() { advance(OrderStatus.DELIVERED, OrderStatus.RETURNED); }
public void cancel() { if (status != OrderStatus.PLACED && status != OrderStatus.PAID) { throw new IllegalStateException("Cannot cancel from " + status); } // release stock; refund will be handled by the support workflow lines.forEach((p, q) -> p.release(q)); status = OrderStatus.CANCELLED; }
private void advance(OrderStatus from, OrderStatus to) { if (status != from) throw new IllegalStateException("Expected " + from + ", was " + status); status = to; }
public OrderStatus status() { return status; } public Money total() { return total; }}Notes the interviewer will look for:
- Saga, not transaction. Checkout is three commands with explicit
compensate()— there is no single database transaction spanning stock, payment, and shipment. - Order ownership of its state machine.
advance(from, to)keeps illegal transitions impossible at the call site. Moneyis its own type. Subtotal, tax, refunds — none aredouble.- Cart and Order are different aggregates. A cart is mutable and pre-checkout; an order is immutable line-items with a status. Conflating them is the regression.
- Compensation runs in reverse order. The
executedlist captures only the steps that completed; rollback walks it from the end.
Trade-offs and extensions#
This section doubles as the what-I-cut list — repeat it out loud at the end of the round.
| Decision | Why | Cost / extension shape |
|---|---|---|
Stock lives on Product | One number per SKU; no warehouse routing in scope. | A real Amazon adds an InventoryService over Warehouses with reservation tokens. Same reserve/release API at the seam. |
CatalogService.search is a seam, not an implementation | Search is an HLD problem. | Plug in a search service; the OOD does not change. |
PaymentStrategy with three concretes | Strategy at the right altitude. | Add UPI / wallet / installments without editing CheckoutService. |
| Checkout saga in-process | No distributed-coordination requirement was given. | At Amazon scale, the saga is durable (Step Functions or equivalent); each command becomes a message. The shape stays. |
Order immutable lines | Once placed, line-items don’t change. | Returns / partial returns add a separate ReturnRequest aggregate. |
| No promotions / coupons / gift wrap | Cut. | PricingPolicy strategy seam on the cart; chain of decorators. |
| No reviews / Q&A | Cut. | Separate aggregates with their own repositories. |
| No recommendations | Cut. | RecommendationService is a seam consumed by the product page renderer. |
| No fraud check | Cut. | A FraudCheckStep slots into the checkout pipeline; same execute/compensate shape. |
| No multi-currency | Cut. | Money carries currency; PricingPolicy returns money in the customer’s currency. |
| No notifications | Cut. | Observer on Order state transitions; channel adapters (email, SMS, push) subscribe. |
Likely follow-up extensions:
- Subscriptions (“Subscribe & Save”). A
Subscriptionaggregate aboveOrderthat schedules a recurringplace(cart)call. Same saga. - Split shipments. One
Orderto manyShipments.Shipmentbecomes a 1..n with a per-shipment status. - Gift orders.
Ordergains an optionalgifting : GiftOptions; nothing else moves. - Marketplace sellers.
Productgains aseller; cart can contain items from multiple sellers;Ordereither fans out into per-seller orders or stays single with per-line shipment tracking.
Mock interview follow-ups#
- “Why is checkout a saga and not a transaction?” — Stock is local state, payment is a third party, shipment is a downstream system. No single transactional boundary spans all three. The compensating actions are well-defined per step, so a saga is the natural shape.
- “Two customers buy the last unit.” —
Product.reserveissynchronized; the loser seesOutOfStockException,LockStockfails, the saga returns failure. No order is created. - “What’s the state machine on
Order?” — Show the activity diagram. EmphasiseCancelledis reachable fromPlaced/Paidonly; oncePicked, the path forward isReturned. - “Where would observers fit?” — On
Orderstate transitions. Notifications, audit, analytics, and downstream warehouse picking can all subscribe withoutOrderknowing. - “How would you persist this?” — One repository per aggregate root:
OrderRepository,CartRepository,ProductRepository. The saga’s commands become idempotent operations against those repositories; the saga itself is durable. - “Add coupons. Where?” —
PricingPolicydecorator chain:BasePrice → CouponDiscount → MembershipDiscount. Cart callsPricingPolicy.priceFor(items). No change toCheckoutService. - “You haven’t talked about search.” — Correct. It’s an HLD round.
CatalogService.searchis the seam; the implementation is sharded, indexed, and ranked, and that conversation deserves its own hour. - “What did you cut and why?” — Recite the list. Decisive cuts are the highest-scoring move in this prompt.
Related#
- Parking Lot — the canonical OOD opener; same
MoneyandStrategydiscipline at a fraction of the scope. - Hotel Management System — the closest sibling: multi-aggregate transaction with compensation.
- State Pattern —
OrderandShipmentare textbook state machines. - Strategy Pattern —
PaymentStrategyandPricingPolicyare clean strategies. - Command Pattern — the checkout saga is the command pattern at saga altitude.