Low-Level Design: Online Shopping Cart (OOP Interview)

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

Scroll to Top