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.
🏢 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