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