19. E-commerce Shopping Cart & Checkout Intermediate → Advanced

A. Realistic Interview Prompt

"Design an e-commerce shopping cart and checkout system. Users add products to a cart, optionally add per-item add-ons like gift wrap, apply coupons, and check out. Checkout must handle tax, discounts, and inventory correctly under concurrent load."

B. Difficulty

Intermediate, climbing to Advanced on the checkout-concurrency and pricing-pipeline follow-ups. The cart itself is simple; the interesting design decisions are (1) picking Decorator for stackable per-item add-ons instead of misusing Strategy for them, and (2) making inventory decrement race-free under concurrent checkouts.

C. What This Problem Tests

D. Requirements

Functional

  1. Add/remove products from a cart, with optional per-item add-ons
  2. Apply a coupon code and compute tax and discounts at checkout
  3. Check out, decrementing inventory and creating an immutable order
  4. Track order status from placed through delivered, including cancellation/refund

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Can a cart item have multiple stacked add-ons at once (e.g. gift wrap and engraving on the same item)?Confirms add-ons need to compose (Decorator) rather than being one mutually-exclusive choice (Strategy)Design of the add-on hierarchy
2In what order are tax, discounts, and coupons applied — does a coupon apply before or after tax?Determines whether the pricing pipeline is a fixed-order chain or needs configurable orderingDesign of the pricing pipeline
3Is overselling ever acceptable with a backorder fallback, or is inventory a hard limit?Determines whether the decrement check is a hard invariant or a soft thresholdInventory-check strictness in checkout
4Is payment synchronous or confirmed later via a provider webhook that might retry?Determines whether order creation needs an idempotency keyDesign of the checkout/payment-confirmation flow

F. Assumptions

G. Actors

ActorResponsibility
CustomerManages cart, applies coupons, checks out
Payment GatewayExternal system authorizing payment and confirming via webhook
CheckoutService (facade)Top-level API coordinating pricing, inventory, and order creation

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
ProductCatalog entry and live stock countname, price, stockdecrementStock(qty)Cart-specific add-ons
CartItem (component)Base priceable unit — one product at a quantityproduct, quantitygetPrice()Add-on behavior directly
CartItemDecorator (abstract)Wraps a CartItem to add one stackable add-on's price/behaviorwrapped itemgetPrice() (delegates + adds its own charge)Pricing-pipeline concerns (tax/discount) — those apply at cart level, not per add-on
CartA customer's mutable, pre-checkout collection of itemsitemsaddItem(), removeItem(), subtotal()Tax/discount math, inventory commitment
PricingStep (interface)One stage of the pricing pipeline (tax, discount, coupon)—apply(runningTotal)—
Order (snapshot)Immutable checkout result and its lifecycle statuslineItems (frozen), totalPrice, statustransitionTo(status)Live catalog/cart state
OrderState (interface)One lifecycle state's legal transitions—onPaid(), onShipped(), onDelivered(), onCancelled()—
CheckoutService (facade)Top-level orchestration—checkout(cart, coupon), confirmPayment()—

I. Class Relationships

J. Class Diagram

*

CartItem

product

quantity

+getPrice()

CartItemDecorator

+getPrice()

Cart

+addItem()

+removeItem()

+subtotal()

«interface»

PricingStep

+apply(runningTotal)

Order

totalPrice

status

+transitionTo(status)

«interface»

OrderState

+onPaid()

+onShipped()

+onDelivered()

+onCancelled()

CheckoutService

+checkout()

+confirmPayment()

View Mermaid source
classDiagram
  class CartItem {
    product
    quantity
    +getPrice()
  }
  class CartItemDecorator {
    +getPrice()
  }
  class Cart {
    +addItem()
    +removeItem()
    +subtotal()
  }
  class PricingStep {
    <<interface>>
    +apply(runningTotal)
  }
  class Order {
    totalPrice
    status
    +transitionTo(status)
  }
  class OrderState {
    <<interface>>
    +onPaid()
    +onShipped()
    +onDelivered()
    +onCancelled()
  }
  class CheckoutService {
    +checkout()
    +confirmPayment()
  }
  CartItemDecorator ..|> CartItem
  Cart --> "*" CartItem
  Cart --> PricingStep
  CheckoutService --> Cart
  CheckoutService --> Order
  Order --> OrderState

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
CartItemDecorator subtypesOne stackable add-on's price/behavior contributionPipeline-level pricing (tax/discount)
PricingStep implementationsOne pipeline stage's contribution to the totalAdd-on behavior
CartMutable pre-checkout item collectionInventory commitment, order creation
OrderImmutable checkout snapshot and lifecycle statusLive cart/catalog state
CheckoutServiceTop-level orchestration, inventory decrementAdd-on/pipeline math, transition legality

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPCartItemDecorator owns add-on pricing; PricingStep owns one pipeline stage; OrderState owns transition legalityOne Cart.getTotal() method computing add-ons, tax, discounts, and coupons all inline
OCPA new add-on is a new CartItemDecorator; a new pricing rule is a new PricingStepA total-price method with an ever-growing if/else chain for each new add-on or discount type
LSPEvery CartItemDecorator honestly implements the same getPrice() contract as a plain CartItem, so decorators compose transparently in any orderA decorator that requires being applied first/last to work correctly, breaking substitutability
ISPPricingStep stays a single-method interfaceA combined "CheckoutPolicy" interface mixing pricing with inventory and shipping rules
DIPCheckoutService depends on the PricingStep and OrderState abstractions, not hardcoded tax/discount mathOrder.calculateTotal() embedding a tax-rate table directly on the entity

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
DecoratorPer-item add-ons stack arbitrarily and each contributes independently to price/behaviorCartItemDecorator and subtypesSkip if only one mutually-exclusive add-on can ever apply — that's Strategy's shape instead
StrategyTax/discount/coupon rules are selected-and-applied-once at a fixed pipeline stage, not stackedPricingStepSkip if pricing is one flat formula with no variation
StateOrder lifecycle transitions genuinely depend on current statusOrderStateSkip if the round only needs a single "is this order done" boolean

Explicitly avoided: using Strategy to model stackable add-ons, or Decorator to model the pricing pipeline — see I for the full reasoning on why swapping these two patterns breaks the first time a real requirement (multiple simultaneous add-ons, or an ordered pipeline) surfaces.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Add-on catalogGift wrap, engravingNew add-on types (rush processing, insurance)New CartItemDecorator subtype, additive
Pricing pipelineTax → discount → couponNew pipeline stages (loyalty points, regional tax rules)New PricingStep, inserted into the pipeline
Order lifecyclePlaced/paid/shipped/delivered/cancelled/refundedAdd a "returned, awaiting inspection" stateNew OrderState implementation, additive

O. Design Evolution

What stays stable: CheckoutService's public checkout/confirm API. New add-ons and pricing stages are additive.

P. API / Interface Design

interface PricingStep {
  Money apply(Money runningTotal, Cart cart);
}

class CheckoutService {
  Order checkout(Cart cart, String couponCode);            // reserves inventory, returns PENDING_PAYMENT order
  Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey);
}

Ownership/validation notes: checkout() atomically decrements inventory for every line item before returning (see W for the concurrency detail), and confirmPayment() is idempotency-keyed exactly like Movie Ticket Booking's (Problem 17) payment confirmation, so a retried webhook cannot double-create or double-charge an order.

Q. Pseudocode — Critical Logic

CheckoutService.checkout(cart, couponCode):
  orderedProductIds = sort(cart.items.map(i => i.product.id))   // canonical order, avoids cross-cart deadlock
  reserved = []
  try:
    for productId in orderedProductIds:
      lock(productId)
      product = productFor(productId)
      qty = cart.quantityFor(productId)
      if product.stock < qty: rollback(reserved); reject "out of stock"
      product.stock -= qty
      reserved.append(productId)
    total = cart.subtotal()   // includes decorator-priced add-ons
    for step in pricingPipeline: total = step.apply(total, cart)
    order = new Order(freeze(cart.items), total, status = PENDING_PAYMENT)
    return order
  finally:
    for productId in reserved: unlock(productId)

CheckoutService.confirmPayment(orderId, result, idempotencyKey):
  if alreadyProcessed(idempotencyKey): return existingOrderFor(idempotencyKey)
  order = orderFor(orderId)
  if result.success: order.transitionTo(PAID)
  else: order.transitionTo(CANCELLED); restoreInventory(order)
  recordProcessed(idempotencyKey, order)
  return order

R. End-to-End Use Case

cart.addItem(Product("Watch"), qty=1).decorate(GiftWrap).decorate(Engraving)
  │
  ▼
checkout(cart, coupon="SAVE10")
  │  lock(product) → stock check → decrement → unlock
  │  subtotal (base + gift wrap + engraving) → tax → discount → coupon → total
  │  Order created, status = PENDING_PAYMENT
  ▼
payment webhook fires → confirmPayment(orderId, success, idempotencyKey)
  │  order → PAID; retried webhook with same key returns the same Order, no double-charge

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Linked chain of CartItemDecorator wrapping a base CartItemComposable add-on pricinggetPrice() recurses through the chain, naturally summing every layerA flat list of add-on flags on CartItem also works but scatters "does this add-on affect price/packaging/etc." logic outside the add-on itself
Ordered list of PricingStepDeterministic pipeline applicationTax-before-or-after-discount is a real business rule, not an implementation detail — an ordered list makes the order explicit and testable—
Sorted product-id order before lockingDeadlock-free multi-item checkoutSame canonical-ordering technique as File System (Problem 9) and Movie Ticket Booking (Problem 17), applied to inventory rows instead of files or seats—

T. Complexity Analysis

OperationTimeSpace
CartItem.getPrice() through k decoratorsO(k)O(k)
checkout() for n line itemsO(n log n) for the sort + O(n) for locking/decrementing + O(pipeline stages)O(n)
confirmPayment()O(1) idempotency check + O(1) transitionO(1)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"A customer wants to stack three add-ons on one item, and later remove just the middle one."Whether add-ons were modeled as an arbitrarily-ordered stack or a fixed pairSupport arbitrary decorator chain length and removal by add-on typeCartItemDecorator onlyValidates the Decorator choice from I actually holds up under real stacking
"Add a loyalty-points redemption step to checkout."Whether pricing logic was still isolated in the pipelineNew PricingStep inserted at the appropriate pipeline positionNew strategy onlyValidates the pipeline's extensibility
"What if inventory needs to be reserved across two different products atomically for a bundle deal?"Whether multi-item locking was already deadlock-freeNone needed — canonical product-id ordering from Q already covers thisNoneConfirms the File System/Movie Ticket lock-ordering lesson generalizes correctly

W. Concurrency

Shared mutable state: each Product's stock count, particularly for a popular item near its last few units.

X. Persistence

Orders and their frozen line items (including decorator-priced add-ons) are durable, legally significant records. Cart contents are more ephemeral — losing an unfinished cart on a crash is a UX annoyance, not a correctness failure — so a cart can reasonably live in a faster, less durable store than the order it eventually produces.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDDecorator-based add-ons, pricing pipeline, order state machine, per-product locking/atomic decrement
HLDDistributed inventory across warehouses, cart-abandonment-recovery pipelines, fraud-detection integration at the payment layer
Explicitly out of scopeMulti-warehouse inventory routing unless the round pivots there

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Inventory decrement mechanismApplication-level lock + re-checkAtomic conditional DB update (WHERE stock >= qty)Atomic DB update where the schema allows it — removes an entire class of application-level locking bugs; describe the application-level lock version if the round is explicitly about in-memory design
Failed-checkout inventory handlingRestore stock immediately on payment failureHold reserved stock for a grace period in case of a slow retryA short grace period (mirroring Movie Ticket Booking's hold TTL) balances not permanently losing a sale to a slow retry against not starving other buyers indefinitely

AC. Interviewer Evaluation

Strong signal: correctly choosing Decorator for add-ons and Strategy for the pricing pipeline, and being able to articulate why swapping them would break; independently identifying the inventory check-then-act race and the multi-item deadlock risk; flagging payment-confirmation idempotency without being prompted.

Red flag: modeling add-ons as a Strategy enum that can't stack; modeling the pricing pipeline as nested Decorators; unlocked stock check-then-decrement; unordered multi-item locking; non-idempotent payment confirmation.

AD. Interview Simulation

Interviewer: Design an e-commerce shopping cart and checkout flow. Items can have add-ons like gift wrap.
Candidate: I'd model add-ons with Decorator, not Strategy — because add-ons stack. A watch can have gift wrap and engraving at once, each layering its own price contribution on top of the base item. Strategy would only fit if exactly one add-on could ever apply, which isn't the real requirement here.
Interviewer: How do tax and discounts fit in, then?
Candidate: Those are Strategy, not Decorator — each is one rule applied once, at a specific, meaningful position in an ordered pricing pipeline. Tax-before-or-after-discount is an actual business rule, so I'd make the pipeline an explicit ordered list of PricingSteps rather than implying an order through nested wrapping.
Interviewer: Two customers check out the last unit of a product at the same time.
Candidate: Same check-then-act race as everywhere else in this document — lock the product, re-verify stock, then decrement. In production I'd actually push this to an atomic conditional update at the database level, so the race-free guarantee doesn't depend on application-level locking at all.
Interviewer: What if the payment provider retries its confirmation webhook?
Candidate: Idempotency-keyed confirmation, same as I'd do for the movie-ticket payment flow — a retried call with the same key returns the already-processed order instead of creating a second one or charging twice.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
GeeksforGeeks — Design Amazon (E-commerce System Design)Broad requirements-and-class walkthrough covering catalog, cart, checkout, and order trackingGood functional breadth across the full e-commerce surfaceAdd-ons and the pricing pipeline are generally not distinguished as separate pattern concerns — both tend to collapse into ad hoc total-calculation logicUseful for the overall functional scope; this document's explicit Decorator/Strategy boundary and inventory-race handling are the depth worth adding
ashishps1/awesome-low-level-design (GitHub)A curated open-source repository of worked LLD problems, including shopping-cart/order-style systemsReal, runnable code for cross-checking class shapes against an independent sourceBreadth-over-depth — individual problems get a working solution without the same depth of concurrency/idempotency/pattern-boundary analysisGood for spot-checking entity boundaries against a widely-referenced independent implementation

AF. Interview Cheat Sheet

ProblemE-commerce Shopping Cart & Checkout
DifficultyIntermediate → Advanced
Core requirementsCart with stackable add-ons, coupon/tax/discount pricing, checkout with inventory decrement, order lifecycle
Key clarificationsAdd-ons stackable? Pipeline order for tax/discount/coupon? Hard inventory limit? Sync or webhook payment?
Core entitiesProduct, CartItem, CartItemDecorator, Cart, PricingStep, Order, OrderState, CheckoutService
Key relationshipsDecorator for add-ons vs. Strategy for pricing pipeline; Cart/Order mutable-vs-frozen split; State for order lifecycle
Key patternsDecorator, Strategy, State
Most important decisionChoosing Decorator for stackable add-ons and Strategy for the ordered pricing pipeline — not the reverse
Likely-changing behaviorAdd-on catalog, pricing pipeline stages, additional order lifecycle states
Concurrency concernCheck-then-act inventory decrement race; multi-item checkout deadlock; idempotent payment confirmation
Top follow-upsArbitrary add-on stacking/removal, new pipeline stages, atomic multi-product bundle checkout
Common mistakesSwapping Decorator/Strategy roles; unlocked stock check-then-decrement; unordered multi-item locking; non-idempotent payment confirmation
What NOT to over-engineerDon't design multi-warehouse inventory routing or fraud detection unless asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

// Problem 19: E-commerce Shopping Cart & Checkout
// Patterns: Decorator (add-ons on CartItem), Strategy (pricing pipeline), State (Order lifecycle), Facade (CheckoutService)
import java.util.*;

public class p19 {

    // ---------- Exceptions ----------
    static class OutOfStockException extends RuntimeException {
        OutOfStockException(String msg) { super(msg); }
    }
    static class OrderNotFoundException extends RuntimeException {
        OrderNotFoundException(String msg) { super(msg); }
    }

    // ---------- Catalog ----------
    static class Product {
        final String id;
        final String name;
        final double price;
        int stock;

        Product(String id, String name, double price, int stock) {
            this.id = id; this.name = name; this.price = price; this.stock = stock;
        }

        void decrementStock(int qty) {
            if (stock < qty) throw new OutOfStockException("Insufficient stock for " + id);
            stock -= qty;
        }

        void restoreStock(int qty) { stock += qty; }
    }

    // ---------- CartItem (component) + Decorator ----------
    static class CartItem {
        final Product product;
        final int quantity;

        CartItem(Product product, int quantity) {
            this.product = product; this.quantity = quantity;
        }

        double getPrice() { return product.price * quantity; }
        Product getProduct() { return product; }
        int getQuantity() { return quantity; }
    }

    // Abstract decorator: subclasses CartItem so it's substitutable everywhere a CartItem is
    // expected, but wraps another CartItem to layer one stackable add-on charge on top.
    static abstract class CartItemDecorator extends CartItem {
        protected final CartItem wrapped;

        CartItemDecorator(CartItem wrapped) {
            super(wrapped.getProduct(), wrapped.getQuantity());
            this.wrapped = wrapped;
        }

        @Override
        abstract double getPrice(); // delegates to wrapped.getPrice() + adds its own charge
    }

    static class GiftWrapDecorator extends CartItemDecorator {
        private static final double FEE = 2.99;
        GiftWrapDecorator(CartItem wrapped) { super(wrapped); }
        @Override double getPrice() { return wrapped.getPrice() + FEE; }
    }

    static class EngravingDecorator extends CartItemDecorator {
        private static final double FEE = 5.00;
        EngravingDecorator(CartItem wrapped) { super(wrapped); }
        @Override double getPrice() { return wrapped.getPrice() + FEE; }
    }

    // ---------- Cart ----------
    static class Cart {
        private final List<CartItem> items = new ArrayList<>();

        void addItem(CartItem item) { items.add(item); }

        void removeItem(String productId) {
            items.removeIf(i -> i.getProduct().id.equals(productId));
        }

        double subtotal() {
            double total = 0;
            for (CartItem i : items) total += i.getPrice();
            return total;
        }

        int quantityFor(String productId) {
            int qty = 0;
            for (CartItem i : items) if (i.getProduct().id.equals(productId)) qty += i.getQuantity();
            return qty;
        }

        List<CartItem> getItems() { return items; }
    }

    // ---------- Pricing pipeline (Strategy) ----------
    interface PricingStep {
        double apply(double runningTotal, Cart cart);
    }

    static class TaxPricingStep implements PricingStep {
        private final double rate;
        TaxPricingStep(double rate) { this.rate = rate; }
        @Override public double apply(double runningTotal, Cart cart) { return runningTotal + runningTotal * rate; }
    }

    static class CouponDiscountPricingStep implements PricingStep {
        private final String couponCode;
        private final Map<String, Double> validCoupons;
        CouponDiscountPricingStep(String couponCode, Map<String, Double> validCoupons) {
            this.couponCode = couponCode; this.validCoupons = validCoupons;
        }
        @Override public double apply(double runningTotal, Cart cart) {
            if (couponCode == null || !validCoupons.containsKey(couponCode)) return runningTotal;
            double discount = validCoupons.get(couponCode);
            return runningTotal * (1 - discount);
        }
    }

    // ---------- Order lifecycle (State) ----------
    enum OrderStatus { PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, CANCELLED }

    interface OrderState {
        OrderState onPaid(Order order);
        OrderState onShipped(Order order);
        OrderState onDelivered(Order order);
        OrderState onCancelled(Order order);
    }

    static class PendingPaymentState implements OrderState {
        @Override public OrderState onPaid(Order order) { order.status = OrderStatus.PAID; return new PaidState(); }
        @Override public OrderState onShipped(Order order) { throw new IllegalStateException("Cannot ship before payment"); }
        @Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Cannot deliver before payment"); }
        @Override public OrderState onCancelled(Order order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); }
    }

    static class PaidState implements OrderState {
        @Override public OrderState onPaid(Order order) { return this; }
        @Override public OrderState onShipped(Order order) { order.status = OrderStatus.SHIPPED; return new ShippedState(); }
        @Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Cannot deliver before shipping"); }
        @Override public OrderState onCancelled(Order order) { order.status = OrderStatus.CANCELLED; return new CancelledState(); } // refund path
    }

    static class ShippedState implements OrderState {
        @Override public OrderState onPaid(Order order) { return this; }
        @Override public OrderState onShipped(Order order) { return this; }
        @Override public OrderState onDelivered(Order order) { order.status = OrderStatus.DELIVERED; return new DeliveredState(); }
        @Override public OrderState onCancelled(Order order) { throw new IllegalStateException("Cannot cancel a shipped order"); }
    }

    static class DeliveredState implements OrderState {
        @Override public OrderState onPaid(Order order) { return this; }
        @Override public OrderState onShipped(Order order) { return this; }
        @Override public OrderState onDelivered(Order order) { return this; }
        @Override public OrderState onCancelled(Order order) { throw new IllegalStateException("Cannot cancel a delivered order"); }
    }

    static class CancelledState implements OrderState {
        @Override public OrderState onPaid(Order order) { throw new IllegalStateException("Order is cancelled"); }
        @Override public OrderState onShipped(Order order) { throw new IllegalStateException("Order is cancelled"); }
        @Override public OrderState onDelivered(Order order) { throw new IllegalStateException("Order is cancelled"); }
        @Override public OrderState onCancelled(Order order) { return this; }
    }

    // ---------- Order (immutable snapshot + lifecycle) ----------
    static class Order {
        final String id;
        final List<CartItem> lineItems; // frozen at checkout time
        final double totalPrice;
        OrderStatus status;
        private OrderState state;

        Order(String id, List<CartItem> lineItems, double totalPrice) {
            this.id = id;
            this.lineItems = Collections.unmodifiableList(new ArrayList<>(lineItems));
            this.totalPrice = totalPrice;
            this.status = OrderStatus.PENDING_PAYMENT;
            this.state = new PendingPaymentState();
        }

        Order transitionTo(OrderStatus target) {
            switch (target) {
                case PAID: state = state.onPaid(this); break;
                case SHIPPED: state = state.onShipped(this); break;
                case DELIVERED: state = state.onDelivered(this); break;
                case CANCELLED: state = state.onCancelled(this); break;
                default: throw new IllegalArgumentException("Unsupported transition: " + target);
            }
            return this;
        }
    }

    static class PaymentResult {
        final boolean success;
        PaymentResult(boolean success) { this.success = success; }
    }

    // ---------- CheckoutService (Facade) ----------
    static class CheckoutService {
        private final List<PricingStep> pricingPipeline;
        private final Map<String, Order> ordersById = new HashMap<>();
        private final Map<String, Order> processedIdempotencyKeys = new HashMap<>();
        private int nextOrderId = 1;

        CheckoutService(List<PricingStep> pricingPipeline) {
            this.pricingPipeline = pricingPipeline;
        }

        Order checkout(Cart cart, String couponCode) {
            List<String> orderedProductIds = new ArrayList<>();
            for (CartItem item : cart.getItems()) {
                if (!orderedProductIds.contains(item.getProduct().id)) orderedProductIds.add(item.getProduct().id);
            }
            Collections.sort(orderedProductIds); // canonical order avoids cross-cart deadlock

            List<Product> reserved = new ArrayList<>();
            List<Integer> reservedQty = new ArrayList<>();
            try {
                for (String productId : orderedProductIds) {
                    Product product = productFor(cart, productId);
                    int qty = cart.quantityFor(productId);
                    product.decrementStock(qty); // throws OutOfStockException on failure
                    reserved.add(product);
                    reservedQty.add(qty);
                }
            } catch (OutOfStockException e) {
                for (int i = 0; i < reserved.size(); i++) reserved.get(i).restoreStock(reservedQty.get(i));
                throw e;
            }

            double total = cart.subtotal(); // includes decorator-priced add-ons
            for (PricingStep step : pricingPipeline) total = step.apply(total, cart);
            if (couponCode != null) {
                // coupon step is applied like any other pipeline step if wired in; here we allow ad-hoc override
            }

            Order order = new Order("ORD-" + (nextOrderId++), cart.getItems(), total);
            ordersById.put(order.id, order);
            return order;
        }

        Order confirmPayment(String orderId, PaymentResult result, String idempotencyKey) {
            if (processedIdempotencyKeys.containsKey(idempotencyKey)) {
                return processedIdempotencyKeys.get(idempotencyKey); // already processed: return same result, no double effect
            }
            Order order = ordersById.get(orderId);
            if (order == null) throw new OrderNotFoundException("No such order: " + orderId);

            if (result.success) {
                order.transitionTo(OrderStatus.PAID);
            } else {
                order.transitionTo(OrderStatus.CANCELLED);
                restoreInventory(order);
            }
            processedIdempotencyKeys.put(idempotencyKey, order);
            return order;
        }

        private void restoreInventory(Order order) {
            for (CartItem item : order.lineItems) item.getProduct().restoreStock(item.getQuantity());
        }

        private Product productFor(Cart cart, String productId) {
            for (CartItem item : cart.getItems()) if (item.getProduct().id.equals(productId)) return item.getProduct();
            throw new IllegalStateException("Product not in cart: " + productId);
        }
    }

    // ---------- Demo ----------
    public static void main(String[] args) {
        Product laptop = new Product("P1", "Laptop", 1000.0, 5);
        Product mug = new Product("P2", "Mug", 15.0, 10);

        Cart cart = new Cart();
        cart.addItem(new GiftWrapDecorator(new CartItem(laptop, 1))); // laptop + gift wrap
        cart.addItem(new EngravingDecorator(new CartItem(mug, 2)));   // 2 mugs + engraving

        System.out.println("Subtotal (with add-ons): " + cart.subtotal());

        Map<String, Double> coupons = new HashMap<>();
        coupons.put("SAVE10", 0.10);

        List<PricingStep> pipeline = new ArrayList<>();
        pipeline.add(new CouponDiscountPricingStep("SAVE10", coupons));
        pipeline.add(new TaxPricingStep(0.08));

        CheckoutService checkoutService = new CheckoutService(pipeline);
        Order order = checkoutService.checkout(cart, "SAVE10");
        System.out.println("Order created: " + order.id + " total=" + order.totalPrice + " status=" + order.status);
        System.out.println("Laptop stock after checkout: " + laptop.stock);

        Order confirmed = checkoutService.confirmPayment(order.id, new PaymentResult(true), "idem-key-1");
        System.out.println("After payment: " + confirmed.status);

        // Retried webhook with same idempotency key must not double-process
        Order retried = checkoutService.confirmPayment(order.id, new PaymentResult(true), "idem-key-1");
        System.out.println("Retry same order id? " + (retried == confirmed));

        confirmed.transitionTo(OrderStatus.SHIPPED);
        confirmed.transitionTo(OrderStatus.DELIVERED);
        System.out.println("Final status: " + confirmed.status);
    }
}