System Design Interview: Design an E-Commerce Order and Checkout System

What Is an E-Commerce Order System?

An e-commerce order system manages the full lifecycle of an online purchase: cart management, checkout, inventory reservation, payment processing, and order fulfillment. Examples: Amazon Orders, Shopify checkout, eBay purchases. Core challenges: concurrent inventory management (two buyers purchasing the last item), payment atomicity, and order state machine across distributed services.

  • Lyft Interview Guide
  • Coinbase Interview Guide
  • DoorDash Interview Guide
  • Airbnb Interview Guide
  • Stripe Interview Guide
  • Shopify Interview Guide
  • System Requirements

    Functional

    • Shopping cart: add/remove items, persist across sessions
    • Checkout: reserve inventory, compute total, process payment
    • Order management: view, cancel, track fulfillment status
    • Inventory management: track stock, prevent overselling

    Non-Functional

    • 100M users, 10K checkouts/second at peak (Black Friday)
    • No overselling: never confirm an order when stock is 0
    • Payment atomicity: charge once per order

    Core Data Model

    products: id, name, price_cents, sku
    inventory: product_id, warehouse_id, quantity_available, quantity_reserved
    carts: id, user_id, created_at, updated_at
    cart_items: cart_id, product_id, quantity, price_at_add
    orders: id, user_id, status, subtotal, tax, shipping, total, created_at
    order_items: order_id, product_id, quantity, unit_price
    payments: id, order_id, amount, status, idempotency_key, provider_charge_id
    

    Cart Implementation

    Carts need fast read/write and do not require ACID guarantees. Store in Redis (TTL 30 days):

    HSET cart:{user_id} product_id:123 quantity:2
    HSET cart:{user_id} product_id:456 quantity:1
    EXPIRE cart:{user_id} 2592000  # 30 days
    

    On checkout: read the cart from Redis, validate prices against current product prices (prices may have changed), then persist as an order in the DB.

    Checkout Flow — Preventing Overselling

    BEGIN TRANSACTION;
    -- 1. Lock inventory rows for all items in the cart
    SELECT quantity_available, quantity_reserved
    FROM inventory
    WHERE product_id IN (123, 456) AND warehouse_id = ?
    FOR UPDATE;
    
    -- 2. Check each product has sufficient quantity
    -- (quantity_available - quantity_reserved >= ordered_quantity)
    
    -- 3. Reserve inventory (not yet deducted -- reserved until fulfilled)
    UPDATE inventory
    SET quantity_reserved = quantity_reserved + ordered_qty
    WHERE product_id = ?;
    
    -- 4. Create order record
    INSERT INTO orders (...) VALUES (...);
    INSERT INTO order_items (...) VALUES (...);
    COMMIT;
    

    If any product check fails: rollback with an “out of stock” error. The transaction ensures atomicity — two concurrent checkouts for the last item both acquire the lock; one succeeds, the other gets the updated (insufficient) quantity and rolls back.

    Payment Processing

    After inventory is reserved, charge the payment method:

    charge = stripe.charge.create(
        amount=total_cents,
        customer=stripe_customer_id,
        idempotency_key=f"order-{order_id}"
    )
    

    On payment success: update order status to CONFIRMED, send confirmation email. On payment failure: release the reserved inventory (decrement quantity_reserved), update order status to PAYMENT_FAILED.

    Order State Machine

    PENDING_PAYMENT → CONFIRMED → PROCESSING → SHIPPED → DELIVERED
          │                │
      PAYMENT_FAILED    CANCELLED (before shipment)
    

    Inventory at Scale

    Sharding inventory by product_id distributes write load. But for flash sales (Nike shoe drop: 10K buyers competing for 1K pairs), the single-product inventory row becomes a hotspot with thousands of concurrent locks. Solutions:

    • Inventory partitioning: split 1K units across 10 virtual warehouse rows (100 units each). Random assignment distributes lock contention 10x.
    • Pre-sale reservation: allow users to queue, assign inventory in batches every 30 seconds.
    • Optimistic locking with retry: use version column instead of FOR UPDATE; retry on conflict (works for low contention, not flash sales).

    Interview Tips

    • Separate cart (Redis) from orders (SQL) — different consistency requirements.
    • quantity_reserved vs quantity_available prevents deducting before payment confirms.
    • Idempotency key = order_id ensures exactly-one charge.
    • Inventory partitioning for flash sales shows awareness of hotspot problems.

    {
    “@context”: “https://schema.org”,
    “@type”: “FAQPage”,
    “mainEntity”: [
    {
    “@type”: “Question”,
    “name”: “How do you prevent overselling when two users buy the last item simultaneously?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Overselling occurs when two transactions both read available quantity = 1, both check it is sufficient, and both decrement — resulting in quantity = -1 and two confirmed orders for one item. Prevention: use SELECT FOR UPDATE inside a database transaction to acquire exclusive row locks on the inventory rows before checking availability. Transaction A runs SELECT quantity FROM inventory WHERE product_id = ? FOR UPDATE. It acquires the lock and reads quantity = 1. Transaction B attempts the same SELECT FOR UPDATE and blocks — it must wait. A decrements quantity to 0 and commits. B now acquires the lock, reads quantity = 0, detects insufficient stock, and rolls back with an out-of-stock error to the user. Granularity: FOR UPDATE locks at the row level (product level), not the table level — concurrent purchases of different products proceed without blocking each other. Alternative for very high contention (flash sales): pre-sell tokens (N tokens for N units, claim a token atomically via DECR in Redis), then process orders asynchronously against claimed tokens.” }
    },
    {
    “@type”: “Question”,
    “name”: “What is the difference between quantity_available and quantity_reserved in inventory?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Using a single quantity column creates a window where inventory is deducted before payment confirms. If payment fails after deduction, you must increment it back — with the risk of double-incrementing on retries. The two-column model separates concerns: quantity_available is the total physical stock. quantity_reserved tracks units held for pending orders (payment not yet confirmed). Available to purchase = quantity_available – quantity_reserved. On checkout: increment quantity_reserved (the item is "spoken for"). On payment success: decrement quantity_available (the item is physically going to the customer). On payment failure: decrement quantity_reserved (release the hold). This way: (1) available_count correctly reflects what other customers can buy during the payment window; (2) double-payment safety — if the payment webhook fires twice, decrementing quantity_available a second time would undercount, so add an idempotency check; (3) cancellations after shipment only decrement quantity_available, since quantity_reserved was already decremented at payment time.” }
    },
    {
    “@type”: “Question”,
    “name”: “How do you handle the shopping cart at scale when users have millions of active carts?”,
    “acceptedAnswer”: { “@type”: “Answer”, “text”: “Shopping carts have different characteristics from orders: they change frequently (add/remove items), do not require ACID guarantees (losing a cart is annoying but not a financial error), and have a natural expiry (30 days of inactivity). Redis is the standard cart store. Key: cart:{user_id} as a Redis hash where field = product_id and value = quantity. Operations: HSET, HGET, HDEL, HGETALL. EXPIRE cart:{user_id} 2592000 (30 days) resets on each update. For 100M active carts at ~500 bytes per cart: 50 GB — fits in a Redis cluster. Durability: Redis AOF (append-only file) provides durability with 1-second granularity. Periodic snapshots to RDB for recovery. On checkout: read the cart, validate each item (price may have changed since added — show user if price increased), then create an order. After successful order creation: DEL cart:{user_id}. Guest carts: use session_id as key; on login, merge guest cart with user cart (taking higher quantity for conflicts).” }
    }
    ]
    }

    Scroll to Top