Low-Level Design: E-Commerce Shopping Cart — Persistence, Pricing, and Checkout

Core Entities

Cart: cart_id, user_id (NULL for guest), session_id (for guest carts), status (ACTIVE, ABANDONED, CHECKED_OUT), currency, created_at, updated_at, expires_at. CartItem: item_id, cart_id, product_id, variant_id (size/color), quantity, unit_price (captured at add-to-cart time), custom_options (JSONB: gift wrap, personalization text). Product: product_id, name, description, base_price, currency, stock_quantity, is_active. ProductVariant: variant_id, product_id, sku, attributes (JSONB: size, color), price_override (NULL = use base_price), stock_quantity. Discount: discount_id, code, type (PERCENTAGE, FIXED_AMOUNT, FREE_SHIPPING, BUY_X_GET_Y), value, min_order_amount, max_uses, uses_count, valid_from, valid_until, applicable_products (NULL = all). CartDiscount: cart_id, discount_id, applied_amount. SavedForLater: user_id, product_id, variant_id, saved_at.

Cart Persistence Strategy

class CartService:
    # Guest carts: stored in Redis with TTL (30 days)
    # Authenticated carts: stored in both Redis (cache) and DB (persistent)

    def get_cart(self, cart_id: str, user_id: Optional[int]) -> Cart:
        # Try Redis first (fast path)
        cached = self.redis.get(f"cart:{cart_id}")
        if cached:
            return Cart.from_json(cached)
        # Cache miss: load from DB (authenticated users only)
        if user_id:
            cart = self.db.get_cart_by_user(user_id)
            if cart:
                self.redis.setex(f"cart:{cart_id}", 86400, cart.to_json())
            return cart
        return None  # guest cart expired

    def add_item(self, cart_id: str, product_id: int,
                 variant_id: int, quantity: int) -> Cart:
        with db.transaction():
            # Validate product availability
            variant = self.db.get_variant(variant_id)
            if not variant.is_available or variant.stock_quantity < quantity:
                raise InsufficientStock(variant_id)

            # Capture current price (price can change after add-to-cart)
            unit_price = variant.price_override or variant.product.base_price

            # Upsert cart item
            existing = self.db.get_cart_item(cart_id, variant_id)
            if existing:
                new_qty = existing.quantity + quantity
                if variant.stock_quantity < new_qty:
                    raise InsufficientStock(variant_id)
                self.db.update_cart_item(existing.item_id, {"quantity": new_qty})
            else:
                self.db.insert_cart_item({
                    "cart_id": cart_id, "product_id": product_id,
                    "variant_id": variant_id, "quantity": quantity,
                    "unit_price": unit_price
                })
            cart = self.db.get_cart(cart_id)
            self.redis.setex(f"cart:{cart_id}", 86400, cart.to_json())
            return cart

Price Calculation and Discount Engine

class PricingEngine:
    def calculate(self, cart: Cart,
                  discount_code: Optional[str] = None) -> CartSummary:
        subtotal = sum(item.unit_price * item.quantity for item in cart.items)

        # Validate and apply discount
        discount_amount = 0
        if discount_code:
            discount = self.db.get_discount(discount_code)
            error = self._validate_discount(discount, cart, subtotal)
            if error:
                raise DiscountError(error)
            discount_amount = self._compute_discount(discount, cart, subtotal)

        # Shipping calculation
        shipping = self._calculate_shipping(cart, subtotal - discount_amount)

        # Tax calculation (by destination address)
        taxable = subtotal - discount_amount + shipping
        tax = self._calculate_tax(taxable, cart.shipping_address)

        return CartSummary(
            subtotal=subtotal,
            discount=discount_amount,
            shipping=shipping,
            tax=tax,
            total=subtotal - discount_amount + shipping + tax
        )

    def _validate_discount(self, d, cart, subtotal) -> Optional[str]:
        if not d or not d.is_active: return "Invalid discount code"
        if d.valid_until = d.max_uses: return "Discount limit reached"
        if subtotal < d.min_order_amount: return f"Minimum order ${d.min_order_amount}"
        return None

Checkout and Inventory Reservation

Checkout flow must atomically reserve inventory and create the order to prevent overselling. Step 1: Price recalculation. Recalculate prices at checkout time (not at add-to-cart time) to catch price changes. Notify user if any price changed. Step 2: Inventory reservation. Use SELECT FOR UPDATE on product_variants rows for all items. Verify stock >= quantity for each item. Decrement stock atomically in the same transaction. If any item is out of stock: rollback and return a specific error per item. Step 3: Order creation. Create Order and OrderItem records with the final prices. Set cart.status = CHECKED_OUT. Step 4: Payment. Create a Stripe PaymentIntent with the final total. On payment failure: reverse inventory reservation (increment stock back). On payment success: confirm the order and trigger fulfillment. Idempotency: the checkout endpoint accepts an idempotency_key (UUID) from the client. If the same key is submitted twice (network retry), return the existing result without double-processing.

See also: Shopify Interview Prep

See also: Stripe Interview Prep

See also: Airbnb Interview Prep

Scroll to Top