CASE STUDY

Shopping Cart and Pricing Service (Uber Eats Cart)

5 min read·862 words·Intermediate

How to use this case study

SDE-2 / Mid

Explain the cart data model, add/update/remove APIs, persistence across devices, and a price breakdown (items, fees, taxes, promos).

SDE-3 / Senior

Go deeper on concurrent edits from two devices, validating price and availability at checkout, exactly-once order creation, and cart expiry.

Staff / Principal

Discuss the pricing engine as extensible rules, caching vs DB for carts, group carts, and consistency with menu changes.


0) Problem Restatement

Design the shopping cart for a food or grocery delivery app. A user picks a restaurant or store, adds items with options ("large, no onions, extra cheese"), changes quantities, and sees a live price breakdown: subtotal, delivery fee, service fee, taxes, discounts and tip. The cart should be the same on the phone and the laptop, and checkout must turn it into exactly one order at the correct price. Uber asked this many times, both as backend design and as class-level (LLD) design of a pricing engine.

Asked at: Uber — 6 candidate reports between Jan 2026 and May 2026.

1) Requirements

1.1 Functional

  • Create a cart (per user, per merchant), and add, update or remove items with customizations.
  • Show the price breakdown, recalculated on every change.
  • Apply promo codes and memberships (e.g., free delivery).
  • Sync across devices.
  • Checkout → create an order (once), with final validation.
  • Expire abandoned carts.

1.2 Non-Functional

  • Fast updates (under 100 ms).
  • Never charge a stale or wrong price. Validate at checkout.
  • No duplicate orders on double-tap or retry.
  • Available: a cart outage blocks all revenue.

1.3 Scale Estimates

  • 20M active carts, 5K cart updates/sec at dinner peak, 500 checkouts/sec.

1.4 API Design

  • GET /v1/carts/current?merchant_id=
  • POST /v1/carts/{id}/items { menu_item_id, qty, options: [...] }{ cart, price_breakdown, version }
  • PATCH /v1/carts/{id}/items/{line_id} { qty } (header If-Match: version)
  • POST /v1/carts/{id}/promo { code }
  • POST /v1/carts/{id}/checkout (Idempotency-Key) → { order_id }


2) High-Level Architecture

Architecture Diagram

flowchart LR
    C["Phone / Web"] --> API["Cart Service"]
    API --> DB[("Cart DB - by user")]
    API --> CA[("Cache - active carts")]
    API --> PE["Pricing Engine"]
    PE --> MENU["Menu / Catalog Service"]
    PE --> PROMO["Promotions Service"]
    PE --> TAX["Tax + Fees Service"]
    API -->|"checkout"| ORD["Order Service"]
    ORD --> PAY["Payments"]
  • Cart Service: owns cart state. It's durable (DynamoDB or Postgres) with a cache for active carts, since carts must survive restarts and switching devices.
  • Pricing Engine: computes the breakdown from the cart plus current menu prices, promotions, fees and taxes. Stateless and deterministic.
  • Order Service: on checkout, creates the order and authorizes payment.


3) Data Model

carts:      cart_id, user_id, merchant_id, status (active, checked_out, expired), version, updated_at, expires_at
cart_lines: cart_id, line_id, menu_item_id, qty, options (JSON), unit_price_snapshot, added_at
cart_promos: cart_id, promo_code
unit_price_snapshot shows the price the user saw. At checkout we compare it with the current price.

4) Pricing Engine (the LLD part)

Model pricing as a pipeline of small, independent rules. Each takes the cart and the current breakdown and adds lines. New fees or promos are just new rule classes, and existing code doesn't change (the open/closed principle).

Architecture Diagram

classDiagram
    class PricingEngine {
        -List rules
        +price(cart, context) Breakdown
    }
    class PricingRule {
        <<interface>>
        +apply(cart, context, breakdown) void
    }
    class ItemSubtotalRule
    class DeliveryFeeRule
    class ServiceFeeRule
    class PromoRule
    class MembershipRule
    class TaxRule
    PricingRule <|.. ItemSubtotalRule
    PricingRule <|.. DeliveryFeeRule
    PricingRule <|.. ServiceFeeRule
    PricingRule <|.. PromoRule
    PricingRule <|.. MembershipRule
    PricingRule <|.. TaxRule
    PricingEngine --> PricingRule
  • Order matters: subtotal → fees → discounts → tax (tax usually applies after discounts, depending on the region's rules).
  • Money as integers (cents). Round only at defined steps.
  • The breakdown lists every line with a label, so the UI shows "Delivery fee $2.99, waived with membership".


5) Key Flows

5.1 Updating the cart

  1. The client sends the change with the cart version.
  2. The service applies it only if the version matches (optimistic concurrency). Otherwise it returns 409 with the latest cart. This handles edits from two devices without losing one silently.
  3. It calls the pricing engine, saves the cart and snapshot prices, bumps the version, and returns the new cart and breakdown.
  4. Other devices get the update via push, or on their next open.

5.2 Checkout

  1. The idempotency key ensures a double tap creates one order.
  2. Re-validate: the merchant is open, items are available, and prices are recalculated from current data. If the total changed, return "prices updated, please review" instead of charging.
  3. Create the order and authorize payment. Mark the cart checked_out (conditional on it still being active, so no double checkout).


6) Edge Cases Worth Mentioning

  • Different restaurant: adding from another merchant asks "start a new cart?". One active cart per merchant or per user, depending on product rules.
  • Menu changed (item removed or price changed) while it was in the cart: flag it in the cart and require confirmation.
  • Promo limits: a one-per-user code is checked at pricing time and reserved at checkout, so two checkouts can't both use it.
  • Expiry: carts idle for 7 days expire (with a TTL). Grocery carts may need item holds for a short time.
  • Group carts: several users add to one shared cart, which uses the same optimistic versioning and per-user lines.


7) Trade-offs & Alternatives

DecisionChoiceWhyAlternative
StorageDurable DB + cacheSurvives restarts, cross-deviceCache/session only: carts lost
ConcurrencyVersion check (optimistic)Simple, handles two devicesLocks: slow, stuck locks
PricingRule pipeline, recomputed on changeExtensible, explainableHard-coded formula: painful changes
CheckoutRe-validate + idempotency keyCorrect price, one orderTrust cart prices: stale charges

8) Wrap-Up

Keep carts durable in a DB (cached when active) with a version for optimistic concurrency across devices. Compute prices with a stateless pricing engine built as an ordered pipeline of rule classes (subtotal, fees, promos, membership, tax) using integer cents. At checkout, use an idempotency key, re-validate availability and prices against current data, reserve promos, and move the cart to checked_out exactly once.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →