Low-Level Design: Online Shopping Cart
The online shopping cart is a fundamental e-commerce OOP design problem. It tests entity modeling (Product, Cart, Order), inventory management, pricing with discounts, and the checkout flow. Common at Shopify, Amazon, and general FAANG OOP rounds.
Core Classes
Enums
from enum import Enum
class OrderStatus(Enum):
PENDING = "PENDING"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
CANCELLED = "CANCELLED"
REFUNDED = "REFUNDED"
class DiscountType(Enum):
PERCENTAGE = "PERCENTAGE"
FIXED = "FIXED"
Product and Inventory
from dataclasses import dataclass, field
@dataclass
class Product:
product_id: str
name: str
base_price: float
category: str
description: str = ""
@dataclass
class InventoryItem:
product: Product
quantity_available: int
def reserve(self, quantity: int) -> None:
if quantity > self.quantity_available:
raise ValueError(
f"Only {self.quantity_available} units of '{self.product.name}' available"
)
self.quantity_available -= quantity
def release(self, quantity: int) -> None:
self.quantity_available += quantity
Discount / Coupon
@dataclass
class Discount:
code: str
discount_type: DiscountType
value: float # percentage (0-100) or fixed amount
min_order_value: float = 0.0
max_uses: int = None
current_uses: int = 0
def is_valid(self, order_subtotal: float) -> bool:
if order_subtotal = self.max_uses:
return False
return True
def apply(self, subtotal: float) -> float:
"""Return the discount amount (not the final price)."""
if not self.is_valid(subtotal):
return 0.0
if self.discount_type == DiscountType.PERCENTAGE:
return round(subtotal * self.value / 100, 2)
return min(self.value, subtotal) # FIXED: can't exceed subtotal
Cart
from typing import Optional
@dataclass
class CartItem:
product: Product
quantity: int
@property
def subtotal(self) -> float:
return round(self.product.base_price * self.quantity, 2)
class Cart:
def __init__(self, user_id: str):
self.user_id = user_id
self.items: dict[str, CartItem] = {} # product_id -> CartItem
self._applied_discount: Optional[Discount] = None
def add_item(self, product: Product, quantity: int = 1) -> None:
if product.product_id in self.items:
self.items[product.product_id].quantity += quantity
else:
self.items[product.product_id] = CartItem(product, quantity)
def remove_item(self, product_id: str) -> None:
self.items.pop(product_id, None)
def update_quantity(self, product_id: str, quantity: int) -> None:
if quantity float:
return round(sum(item.subtotal for item in self.items.values()), 2)
def apply_discount(self, discount: Discount) -> float:
if not discount.is_valid(self.subtotal):
raise ValueError(f"Discount code '{discount.code}' is not valid for this cart")
self._applied_discount = discount
saved = discount.apply(self.subtotal)
print(f"Discount applied: -{saved:.2f}")
return saved
@property
def total(self) -> float:
subtotal = self.subtotal
if self._applied_discount:
discount_amount = self._applied_discount.apply(subtotal)
return round(subtotal - discount_amount, 2)
return subtotal
def clear(self) -> None:
self.items.clear()
self._applied_discount = None
Order
import uuid
from datetime import datetime
@dataclass
class OrderItem:
product: Product
quantity: int
unit_price: float # price locked at time of order
@property
def subtotal(self) -> float:
return round(self.unit_price * self.quantity, 2)
@dataclass
class Order:
order_id: str
user_id: str
items: list[OrderItem]
total: float
status: OrderStatus = OrderStatus.PENDING
created_at: datetime = field(default_factory=datetime.now)
def cancel(self) -> None:
if self.status not in (OrderStatus.PENDING, OrderStatus.CONFIRMED):
raise ValueError(f"Cannot cancel order in status {self.status.value}")
self.status = OrderStatus.CANCELLED
def ship(self) -> None:
if self.status != OrderStatus.CONFIRMED:
raise ValueError("Only CONFIRMED orders can be shipped")
self.status = OrderStatus.SHIPPED
ShoppingService (Orchestrator)
class ShoppingService:
def __init__(self):
self.inventory: dict[str, InventoryItem] = {} # product_id -> InventoryItem
self.carts: dict[str, Cart] = {} # user_id -> Cart
self.orders: dict[str, Order] = {} # order_id -> Order
self.discounts: dict[str, Discount] = {} # code -> Discount
def add_product(self, product: Product, quantity: int) -> None:
self.inventory[product.product_id] = InventoryItem(product, quantity)
def get_or_create_cart(self, user_id: str) -> Cart:
if user_id not in self.carts:
self.carts[user_id] = Cart(user_id)
return self.carts[user_id]
def add_to_cart(self, user_id: str, product_id: str, quantity: int = 1) -> None:
inv = self.inventory.get(product_id)
if not inv:
raise ValueError(f"Product {product_id} not found")
if inv.quantity_available Order:
cart = self.carts.get(user_id)
if not cart or not cart.items:
raise ValueError("Cart is empty")
# Reserve inventory for each item
reserved = []
try:
for cart_item in cart.items.values():
inv = self.inventory[cart_item.product.product_id]
inv.reserve(cart_item.quantity)
reserved.append((inv, cart_item.quantity))
except ValueError:
# Rollback all reservations
for inv, qty in reserved:
inv.release(qty)
raise
# Create order with prices locked at checkout time
order_items = [
OrderItem(ci.product, ci.quantity, ci.product.base_price)
for ci in cart.items.values()
]
order = Order(
order_id=str(uuid.uuid4()),
user_id=user_id,
items=order_items,
total=cart.total,
status=OrderStatus.CONFIRMED,
)
self.orders[order.order_id] = order
# Increment discount usage
if cart._applied_discount:
cart._applied_discount.current_uses += 1
cart.clear()
print(f"Order {order.order_id} confirmed. Total: $" + f"{order.total:.2f}")
return order
Interview Follow-ups
- Price locking: OrderItem stores unit_price at checkout time — protects against price changes after order is placed.
- Inventory rollback: If reserving item 3 fails after items 1 and 2 were reserved, release items 1 and 2. This is a compensating transaction pattern.
- Concurrent checkout: Use a per-product lock before reserve(). In distributed systems: database transaction with SELECT FOR UPDATE on inventory rows.
- Cart persistence: Redis HASH for cart items (field=product_id, value=quantity) with TTL=7 days. Falls back to DB on cache miss.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a shopping cart with inventory reservation?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Two-phase approach: (1) Add to cart — validate stock availability but do NOT reserve. Cart items are advisory. (2) Checkout — atomically reserve inventory for all items. If any item is out of stock during checkout, roll back all reservations made so far (compensating transaction) and return an error. In code: iterate cart items, call inventory.reserve(qty) for each. If reserve() raises an exception (insufficient stock), release all previously reserved items and re-raise. This ensures checkout is all-or-nothing. Why not reserve at add-to-cart? Reserving at cart addition ties up inventory for hours while users browse, effectively preventing others from purchasing in-demand items. Reservation at checkout is the right tradeoff — brief window where two users could both see stock as available, but only one completes checkout successfully.”}},{“@type”:”Question”,”name”:”How do you implement discount codes in a shopping cart?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Model a Discount with: code (string), discount_type (PERCENTAGE or FIXED), value (percent or dollar amount), min_order_value (minimum cart subtotal to apply), max_uses (optional usage limit), and current_uses counter. The is_valid() method checks: subtotal >= min_order_value and current_uses < max_uses. The apply(subtotal) method computes the discount amount — for PERCENTAGE: subtotal * value/100; for FIXED: min(value, subtotal) so the discount never exceeds the cart total. Apply the discount to the subtotal before tax/shipping. Lock in the discount amount at checkout time (not at order creation) to prevent re-applying after price changes. Increment current_uses atomically at checkout completion — use a database transaction or Redis INCR to prevent race conditions where two users simultaneously use a single-use coupon."}},{"@type":"Question","name":"How do you handle price changes between add-to-cart and checkout?","acceptedAnswer":{"@type":"Answer","text":"Lock the price at checkout, not at cart addition. The Cart computes total using the product's current base_price (live pricing). When checkout() creates an Order, it creates OrderItem records with unit_price = product.base_price at that moment. This means: if a price changes between the user adding an item and checking out, the cart subtotal displayed during browsing may be different from the checkout price — this is the standard e-commerce behavior (prices can change; the checkout price is binding). The OrderItem.unit_price is immutable after order creation — historical order records always show what the customer paid. To show users when a price changed: compare current base_price with the price when the item was added to the cart (store added_price on CartItem) and display a "price changed" warning during checkout review. This is what Amazon does for price-drop alerts."}}]}
🏢 Asked at: Shopify Interview Guide
🏢 Asked at: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
🏢 Asked at: Coinbase Interview Guide
🏢 Asked at: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering
🏢 Asked at: DoorDash Interview Guide