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