Low-Level Design: Shopping Cart and Checkout (Inventory, Coupons, Payments)

Low-Level Design: Shopping Cart and Checkout System

A shopping cart system manages item selection, inventory reservation, coupon application, pricing, and checkout. It is a common LLD question at e-commerce companies (Amazon, Shopify, Stripe).

Core Entities


from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import uuid

class DiscountType(Enum):
    PERCENTAGE = "percentage"
    FIXED_AMOUNT = "fixed_amount"
    BUY_X_GET_Y = "buy_x_get_y"

@dataclass
class Product:
    product_id: str
    name: str
    price_cents: int       # integer cents to avoid float errors
    stock: int
    category: str

@dataclass
class Coupon:
    code: str
    discount_type: DiscountType
    discount_value: float   # percentage (0-100) or fixed cents
    min_order_cents: int    # minimum order to apply
    max_uses: int
    used_count: int = 0
    is_active: bool = True

    def is_valid(self) -> bool:
        return self.is_active and self.used_count  int:
        return self.unit_price_cents * self.quantity

@dataclass
class Cart:
    cart_id: str
    user_id: str
    items: list[CartItem] = field(default_factory=list)
    coupon_code: Optional[str] = None

    @property
    def subtotal_cents(self) -> int:
        return sum(item.subtotal_cents for item in self.items)

    @property
    def item_count(self) -> int:
        return sum(item.quantity for item in self.items)

Cart Operations


class CartService:
    def __init__(self, product_store, coupon_store, inventory_service):
        self._carts: dict[str, Cart] = {}  # cart_id -> Cart
        self._user_carts: dict[str, str] = {}  # user_id -> cart_id
        self.products = product_store
        self.coupons = coupon_store
        self.inventory = inventory_service

    def get_or_create_cart(self, user_id: str) -> Cart:
        if user_id in self._user_carts:
            return self._carts[self._user_carts[user_id]]
        cart_id = str(uuid.uuid4())
        cart = Cart(cart_id=cart_id, user_id=user_id)
        self._carts[cart_id] = cart
        self._user_carts[user_id] = cart_id
        return cart

    def add_item(self, user_id: str, product_id: str, quantity: int) -> CartItem:
        cart = self.get_or_create_cart(user_id)
        product = self.products.get(product_id)
        if not product:
            raise ValueError(f"Product {product_id} not found")
        if product.stock < quantity:
            raise ValueError(f"Insufficient stock: {product.stock} available")

        # Update existing item or add new
        for item in cart.items:
            if item.product_id == product_id:
                new_qty = item.quantity + quantity
                if product.stock  None:
        cart = self.get_or_create_cart(user_id)
        cart.items = [i for i in cart.items if i.product_id != product_id]

    def update_quantity(self, user_id: str, product_id: str, quantity: int) -> None:
        if quantity <= 0:
            return self.remove_item(user_id, product_id)
        cart = self.get_or_create_cart(user_id)
        product = self.products.get(product_id)
        if product.stock  dict:
        cart = self.get_or_create_cart(user_id)
        coupon = self.coupons.get(code.upper())
        if not coupon or not coupon.is_valid():
            raise ValueError(f"Coupon '{code}' is invalid or expired")
        if cart.subtotal_cents  dict:
        subtotal = cart.subtotal_cents
        discount = 0
        if coupon:
            if coupon.discount_type == DiscountType.PERCENTAGE:
                discount = int(subtotal * coupon.discount_value / 100)
            elif coupon.discount_type == DiscountType.FIXED_AMOUNT:
                discount = min(int(coupon.discount_value), subtotal)
        tax = int((subtotal - discount) * 0.08)  # 8% tax
        total = subtotal - discount + tax
        return {
            'subtotal_cents': subtotal,
            'discount_cents': discount,
            'tax_cents': tax,
            'total_cents': total,
        }

Checkout with Inventory Reservation


class CheckoutService:
    def __init__(self, cart_svc, inventory_svc, payment_gateway, order_store):
        self.carts = cart_svc
        self.inventory = inventory_svc
        self.payments = payment_gateway
        self.orders = order_store

    def checkout(self, user_id: str, payment_method: str) -> str:
        cart = self.carts.get_or_create_cart(user_id)
        if not cart.items:
            raise ValueError("Cart is empty")

        # Phase 1: Reserve inventory (optimistic lock)
        reserved = []
        try:
            for item in cart.items:
                self.inventory.reserve(item.product_id, item.quantity)
                reserved.append((item.product_id, item.quantity))
        except Exception as e:
            # Roll back successful reservations
            for product_id, qty in reserved:
                self.inventory.release(product_id, qty)
            raise ValueError(f"Inventory reservation failed: {e}")

        # Phase 2: Process payment
        totals = self.carts._compute_totals(cart)
        try:
            payment_id = self.payments.charge(
                amount_cents=totals['total_cents'],
                method=payment_method,
                description=f"Order for user {user_id}",
            )
        except Exception as e:
            for product_id, qty in reserved:
                self.inventory.release(product_id, qty)
            raise ValueError(f"Payment failed: {e}")

        # Phase 3: Create order
        order_id = self.orders.create({
            'user_id': user_id,
            'items': cart.items,
            'totals': totals,
            'payment_id': payment_id,
            'coupon_code': cart.coupon_code,
        })

        # Phase 4: Commit inventory, clear cart
        for product_id, qty in reserved:
            self.inventory.commit(product_id, qty)
        self.carts._carts.pop(cart.cart_id, None)
        self.carts._user_carts.pop(user_id, None)
        return order_id

Design Decisions

Decision Choice Rationale
Price storage Integer cents Avoids float arithmetic errors
Coupon types Strategy via DiscountType enum Extensible: add BUY_X_GET_Y without changing apply_coupon()
Inventory reservation Two-phase (reserve → commit) Prevents overselling; rolls back on payment failure
Cart storage In-memory for LLD; Redis for prod Carts are ephemeral, not critical data; fast reads
Cart per user One active cart per user_id Simplest model; merge logic needed for guest→logged-in transition

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “Why store prices in integer cents instead of floats?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Floating-point arithmetic is inexact for decimal fractions: 0.1 + 0.2 = 0.30000000000000004 in IEEE 754. For a shopping cart, this causes rounding errors that accumulate across items, discounts, and taxes, potentially resulting in a different total than the sum of displayed line items u2014 a billing discrepancy. Storing amounts in integer cents (or pence, etc.) makes all arithmetic exact integer operations. Display divides by 100; all internal computation is integer math.”
}
},
{
“@type”: “Question”,
“name”: “How do you prevent overselling during a flash sale?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Two-phase inventory reservation at checkout: (1) Reserve: atomically decrement available stock (UPDATE inventory SET available = available – qty WHERE product_id = X AND available >= qty) u2014 if 0 rows updated, reject. (2) Commit: after payment succeeds, finalize the reservation. If payment fails, release the reservation. For very high traffic, use optimistic locking with a version column, or a Redis DECR with a floor check (DECRBY with Lua script to prevent going negative). Reservations should time out (e.g., 10 minutes) if not committed.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle the guest-to-logged-in cart merge?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store guest cart in a cookie-backed session with a temporary cart_id. On login, fetch both the guest cart (from session) and the user’s existing cart (from database, if any). Merge: for items only in guest cart, add to user cart; for items in both, take the higher quantity or guest quantity (depending on product); keep the user’s existing coupon. Delete the guest session cart. This prevents losing items a user added before remembering to log in u2014 a common UX requirement.”
}
},
{
“@type”: “Question”,
“name”: “How do you apply stacked discounts (coupon + sale price + membership)?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Define a discount pipeline with ordered steps: (1) Apply product-level sale prices (unit price already reduced in catalog). (2) Apply coupon on the subtotal. (3) Apply membership discount on the post-coupon total. (4) Calculate tax on the final discounted amount. Use the Strategy pattern: each discount type implements a calculate(subtotal) u2192 discount_amount interface. The pipeline composes strategies in priority order. Store the discount breakdown per order for receipts, refund calculations, and auditing.”
}
},
{
“@type”: “Question”,
“name”: “How would you scale a shopping cart system for Black Friday traffic?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Cart state is ephemeral u2014 store in Redis (HASH per cart_id) rather than a relational database. This gives O(1) HSET/HGET operations and horizontal scaling via Redis Cluster. Pre-warm the inventory cache (Redis) before the sale; use a write-through cache pattern. For checkout, use a queue (Kafka) to decouple inventory reservation from payment u2014 the checkout endpoint enqueues a request and returns a pending order_id; a worker processes reservation + payment asynchronously. This prevents the database from being overwhelmed by simultaneous checkouts.”
}
}
]
}

Asked at: Shopify Interview Guide

Asked at: Stripe Interview Guide

Asked at: Airbnb Interview Guide

Asked at: DoorDash Interview Guide

Scroll to Top