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 }(headerIf-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
- The client sends the change with the cart
version. - 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.
- It calls the pricing engine, saves the cart and snapshot prices, bumps the version, and returns the new cart and breakdown.
- Other devices get the update via push, or on their next open.
5.2 Checkout
- The idempotency key ensures a double tap creates one order.
- 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.
- Create the order and authorize payment. Mark the cart
checked_out(conditional on it still beingactive, 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
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Storage | Durable DB + cache | Survives restarts, cross-device | Cache/session only: carts lost |
| Concurrency | Version check (optimistic) | Simple, handles two devices | Locks: slow, stuck locks |
| Pricing | Rule pipeline, recomputed on change | Extensible, explainable | Hard-coded formula: painful changes |
| Checkout | Re-validate + idempotency key | Correct price, one order | Trust 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.