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

Asked at: Shopify Interview Guide

Asked at: Stripe Interview Guide

Asked at: Airbnb Interview Guide

Asked at: DoorDash Interview Guide

Scroll to Top