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.

{“@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

Scroll to Top