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

Source: https://www.techinterview.org/post/3233466457/lld-ecommerce-cart/
Updated: 2026-04-17 · techinterview.org

## 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](https://techinterview.org/shopify/)

See also: [Stripe Interview Prep](https://techinterview.org/stripe/)

See also: [Airbnb Interview Prep](https://techinterview.org/airbnb/)
