Designing a Cart Management System (Amazon / Flipkart / JioMart)

Difficulty: Intermediate Prerequisites:Caching and Message Queues


TL;DR

A cart management system lets users add, remove, and update items before checkout. The hard parts: persisting carts across devices, handling flash sales (1M users adding the same item), merging guest carts on login, and keeping prices/stock accurate without slowing down the add-to-cart path.

flowchart LR
    USER["User Browser"]:::client
    GW["API Gateway"]:::edge
    CART["Cart Service"]:::service
    REDIS[("Redis<br/>hot carts")]:::data
    DDB[("DynamoDB<br/>durable carts")]:::data
    INV["Inventory Service"]:::service

    USER -->|"1. Add item to cart"| GW
    GW -->|"2. Forward request"| CART
    CART -->|"3. Write cart fast"| REDIS
    CART -.->|"4. Async persist"| DDB
    CART -->|"5. Check stock on read"| INV

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef edge fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
Color Layer
🟠 Orange Clients
🔵 Blue Edge
🟢 Green Services
🟣 Purple Async / Streaming
🟡 Yellow Data
🩷 Pink External

In 3 sentences: User adds items to cart → stored in Redis for sub-ms reads and async-backed to DynamoDB for durability. On cart view, the service enriches items with live price and stock status from downstream services. At checkout, inventory is reserved with a distributed lock to prevent overselling.


1. Understanding the Problem

A shopping cart is the bridge between browsing and buying. Every e-commerce platform needs one, and despite looking simple (just a list of items), it has to handle: millions of concurrent carts during sales, real-time stock awareness, device switching (add on phone, checkout on laptop), guest-to-login transitions, flash sales where 100K users add the same item simultaneously, and graceful degradation when downstream services (inventory, pricing) are slow or down.

Think Amazon Cart, Flipkart Cart, Myntra Bag, or Zepto Cart.


1.5. Naive First Cut

flowchart LR
    USER["User"]:::client
    API["Cart API"]:::service
    DB[("One Postgres DB<br/>carts table")]:::data

    USER --> API
    API --> DB

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

User adds item → write a row to Postgres → read it back on cart page. Works for 100 users.

How it breaks at scale:


1.7. Prior Art We’re Drawing From


2. Functional Requirements

Core (Top 3)

  1. Add, remove, update items — user adds an item to cart, changes quantity, or removes it. Response in <100ms.
  2. Persistent cross-device cart — logged-in user’s cart survives app close, device switch, and browser restart. Guest carts persist via session cookie (7-day TTL).
  3. Real-time cart enrichment — when user views cart, each item shows current price, stock status (in stock / only 2 left / out of stock), and delivery estimate.

Below the Line


3. Non-Functional Requirements

Core

NFR Target
Latency Add-to-cart: P99 < 100ms. Cart page load: P99 < 200ms
Availability 99.99% — cart must never be “down” (it’s pre-checkout, losing it = lost revenue)
Scale 50M DAU, 10M concurrent active carts, 500K add-to-cart ops/sec at peak (flash sale)
Durability Logged-in user’s cart must never be lost. Acceptable: guest carts can be lost on infra failure (they’re session-tied).
Consistency Eventually consistent for cart reads (5s stale is fine). Strongly consistent for inventory reservation at checkout.

Below the Line


Scale Estimation


Technology Choices

Tier Purpose Stores Access Pattern Primary Alternatives
Hot cart store Active carts (session-like, fast) userId → {items} High-QPS reads + writes, sub-ms Redis Cluster Memcached, Aerospike
Durable cart store Cart backup, survives Redis failure userId → {items, metadata} Write-behind from Redis, fallback reads DynamoDB Cassandra, ScyllaDB, MongoDB
Cart events Analytics, abandoned cart triggers cart_updated, item_added events Append-only, consumed by analytics Kafka Kinesis, Pub/Sub
Inventory service Stock levels per item itemId → quantity_available High-QPS reads, strong consistency for reservation Postgres (with row locks for checkout) DynamoDB conditional writes
Pricing service Current price per item itemId → price, discounts Read-heavy, cacheable Redis cache + Postgres source Pricing microservice
Session store Guest cart → session mapping sessionId → guestCartId Short-lived, TTL-based Redis (with 7-day TTL) Cookie-only (limited)

Why Redis + DynamoDB (not just Postgres):

Cart is session-like data — high write rate, high read rate, no complex joins, flexible schema (items vary per seller). Postgres row locks collapse at 500K writes/sec. Redis gives sub-ms latency for the hot path. DynamoDB provides infinite-scale durability without managing shards.


4. Core Entities


5. API / System Interface

POST /v1/cart/items
  Body: { itemId, sellerId, quantity }
  Response: { cartId, items[], totalItems, version }
  Auth: JWT (logged-in) or Session cookie (guest)
  Note: Idempotent via clientRequestId header. Does NOT check inventory (fire-and-forget add).

PATCH /v1/cart/items/{itemId}
  Body: { quantity }
  Response: { cartId, items[], version }
  Note: quantity=0 is equivalent to DELETE

DELETE /v1/cart/items/{itemId}
  Response: { cartId, items[], version }

GET /v1/cart
  Response: { cartId, items[{ itemId, quantity, currentPrice, stockStatus, deliveryEstimate }], totalAmount, version }
  Note: Enriched response — each item has LIVE price and stock from downstream services.
  Auth: JWT or Session cookie

POST /v1/cart/merge
  Body: { guestSessionId }
  Response: { cartId, items[], mergeConflicts[] }
  Note: Called on login. Merges guest cart into user cart.

POST /v1/cart/checkout
  Response: { orderId, reservationId, paymentUrl }
  Note: This is where inventory gets RESERVED (locked). Not before.

Security notes:


6. High-Level Design

FR1: Add / Remove / Update Items

The most frequent operation. Must be blazing fast — users expect instant feedback when clicking “Add to Cart.”

New components:

  1. API Gateway — rate limiting, auth, routing
  2. Cart Service — core business logic for cart operations
  3. Redis Cluster — hot cart storage. Each user’s cart is a Redis Hash.
    💡 Redis Hash: HSET cart:{userId} {itemId} “{qty, sellerId, priceAtAdd, addedAt}”. O(1) per field operation.
  4. DynamoDB — durable backup. Async write-behind from Redis.
  5. Kafka — emits cart events for analytics and abandoned-cart workflows
flowchart LR
    USER["User"]:::client
    GW["API Gateway"]:::edge
    CS["Cart Service"]:::service
    REDIS[("Redis Cluster<br/>hot carts")]:::data
    DDB[("DynamoDB<br/>durable backup")]:::data
    KF["Kafka"]:::async

    USER -->|"1. POST add item"| GW
    GW -->|"2. Auth + rate limit"| CS
    CS -->|"3. HSET cart:userId"| REDIS
    CS -.->|"4. Async write-behind"| DDB
    CS -->|"5. Emit item_added event"| KF

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef edge fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
    classDef async fill:#3b1f5e,stroke:#c084fc,color:#e2e8f0

Step-by-step flow:

  1. User taps “Add to Cart” for itemId=SKU_123, quantity=1
  2. Request hits API Gateway → validates JWT → extracts userId → forwards to Cart Service
  3. Cart Service executes Redis command: HSET cart:{userId} SKU_123 '{"qty":1,"seller":"seller_abc","price":499,"addedAt":"..."}'
  4. Redis responds in <1ms → Cart Service returns 200 OK to user immediately
  5. Async: Cart Service publishes to Kafka topic cart-events: {type: "ITEM_ADDED", userId, itemId, qty, timestamp}
  6. Async: A write-behind consumer reads from Kafka and persists to DynamoDB (batched, every 5s or 100 events)

Why we do NOT check inventory at add-time:


FR2: Persistent Cross-Device Cart + Guest Merge

User adds items on phone → closes app → opens laptop → cart is there. Guest browses without login → adds 3 items → logs in → those items appear in their saved cart.

New components:

  1. Session Service — maps guest session cookies to temporary cart IDs in Redis
  2. Cart Merge Logic — handles conflicts when guest cart and user cart have overlapping items
flowchart LR
    GUEST["Guest User"]:::client
    LOGGED["Logged-in User"]:::client
    CS["Cart Service"]:::service
    REDIS[("Redis<br/>both carts")]:::data
    MERGE["Merge Logic"]:::service

    GUEST -->|"1. Add items (session cookie)"| CS
    CS -->|"2. Write to guest cart"| REDIS
    LOGGED -->|"3. Login triggers merge"| CS
    CS -->|"4. Read guest cart"| REDIS
    CS -->|"5. Read user cart"| REDIS
    MERGE -->|"6. Merge + write final"| REDIS

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

Step-by-step flow:

  1. Guest browses → session cookie assigned → cart stored at cart:guest:{sessionId} in Redis with 7-day TTL
  2. Guest adds items → same flow as FR1 but keyed by sessionId instead of userId
  3. Guest logs in → frontend calls POST /v1/cart/merge { guestSessionId }
  4. Cart Service reads both carts from Redis: guest cart + user’s existing cart
  5. Merge strategy (per item):
    • Item only in guest cart → add to user cart
    • Item only in user cart → keep as-is
    • Item in both → take higher quantity (user intent: they wanted more)
  6. Write merged cart to cart:{userId}, delete cart:guest:{sessionId}
  7. Return merged cart to frontend with any conflicts highlighted

Cross-device persistence:


FR3: Real-Time Cart Enrichment (Price + Stock)

When user opens the cart page, they don’t just see “iPhone × 1.” They see: current price (might have changed since they added it), stock status (“only 2 left”), delivery estimate, and a warning if price increased.

New components:

  1. Inventory Service — returns current stock level per item
  2. Pricing Service — returns current price (may differ from price-at-add-time)
  3. Catalog Service — returns item details (name, image, seller info)
flowchart LR
    USER["User opens cart"]:::client
    CS["Cart Service"]:::service
    REDIS[("Redis<br/>cart items")]:::data
    INV["Inventory Service"]:::service
    PRICE["Pricing Service"]:::service
    CAT["Catalog Service"]:::service

    USER -->|"1. GET /cart"| CS
    CS -->|"2. HGETALL cart:userId"| REDIS
    CS -->|"3. Batch stock check"| INV
    CS -->|"4. Batch price fetch"| PRICE
    CS -->|"5. Batch item details"| CAT
    CS -->|"6. Return enriched cart"| USER

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0

Step-by-step flow:

  1. User opens cart page → GET /v1/cart
  2. Cart Service reads raw cart from Redis: HGETALL cart:{userId} → returns {SKU_123: qty 1, SKU_456: qty 2}
  3. Cart Service calls downstream services IN PARALLEL (not sequential):
    • Inventory: POST /inventory/batch-check { itemIds: [SKU_123, SKU_456] } → {SKU_123: 15 in stock, SKU_456: 0}
    • Pricing: POST /pricing/batch { itemIds: [...] } → {SKU_123: ₹499 (same), SKU_456: ₹1299 (was ₹999)}
    • Catalog: POST /catalog/batch { itemIds: [...] } → {names, images, seller info}
  4. Cart Service assembles enriched response:
    {
      "items": [
        { "itemId": "SKU_123", "qty": 1, "currentPrice": 499, "priceAtAdd": 499, "stock": "IN_STOCK", "stockQty": 15 },
        { "itemId": "SKU_456", "qty": 2, "currentPrice": 1299, "priceAtAdd": 999, "stock": "OUT_OF_STOCK", "priceChanged": true }
      ]
    }
    
  5. Frontend shows: “Price increased ₹999 → ₹1299” badge and “Out of Stock — Remove” option

Graceful degradation: If inventory service is slow/down → return cart without stock info (show “checking availability…” on frontend). Cart should NEVER fail to load because a downstream enrichment service is down.


7. Potential Deep Dives

Deep Dive 1: Flash Sale — 500K Users Add the Same Item Simultaneously

Problem: Big Billion Days. iPhone listed at ₹39,999. 500K users click “Add to Cart” in the first 10 seconds. Only 1000 units available.

Bad: Check inventory on every add-to-cart. Inventory service gets 500K reads/sec for one item → hot key → collapses. Users see “item unavailable” errors.

Good: Don’t check inventory at add-time. Let everyone add to cart freely. Check stock only on cart page view and at checkout. Show “limited stock” badge. First 1000 to checkout win.

Great: Same as Good, but add a soft limit at the cart level. After the first 2000 add-to-carts for a flash-sale item, start showing “selling fast — checkout now” urgency messaging. Use Redis INCR on a counter flash:{itemId}:carts to track how many carts contain this item. This doesn’t block anyone but creates urgency and reduces phantom demand.

At checkout: use DynamoDB conditional write (or Postgres SELECT FOR UPDATE) to atomically decrement inventory. If inventory hits 0, reject checkout for subsequent users. Their cart still has the item, but they see “sold out” when they try to pay.

Key insight: Adding to cart is NOT a commitment to buy. Don’t waste inventory-service capacity on something that has a 70% abandonment rate.


Deep Dive 2: Write-Behind Pattern — Redis + DynamoDB Consistency

Problem: We write to Redis first (fast) and async-persist to DynamoDB (durable). What if Redis crashes after the write but before the DynamoDB persist? User’s last few cart changes are lost.

Bad: Write to DynamoDB synchronously on every cart operation. Adds 5-10ms latency to every add/remove. At 500K ops/sec, DynamoDB costs explode (on-demand pricing).

Good: Write-behind with Kafka as buffer. Cart Service writes to Redis AND publishes to Kafka. A consumer drains Kafka into DynamoDB in batches. If Redis crashes, Kafka still has the events → replay into DynamoDB → hydrate back to Redis from DynamoDB.

Great: Same as Good + version vector on the cart. Each cart has a monotonically increasing version field. Redis cart has version 47. DynamoDB has version 45 (slightly behind). On Redis failure:

  1. Cart Service falls back to reading DynamoDB (version 45)
  2. When Redis recovers, hydrate from DynamoDB
  3. If user made changes during the fallback window, those are already in DynamoDB (the consumer was writing there via Kafka)
  4. No data loss. At most 2-3 seconds of stale reads during the Redis→DynamoDB switchover.

Deep Dive 3: Cart Merge Conflicts

Problem: Guest has: {iPhone: qty 2, AirPods: qty 1}. Logged-in cart has: {iPhone: qty 1, MacBook: qty 1}. User logs in. What does the merged cart look like?

Bad: Overwrite user cart with guest cart (lose MacBook). Or overwrite guest with user (lose AirPods).

Good: Union of both carts. For conflicts (same item in both), take max quantity. Result: {iPhone: 2, AirPods: 1, MacBook: 1}.

Great: Same as Good but with user transparency. Show a toast: “We added 1 item from your browsing session” or a merge summary. For quantity conflicts, default to max but let user adjust on the cart page. Log merge events to Kafka for analytics (how many users hit merge conflicts? is it causing confusion?).

Edge case: guest cart has item that’s now out of stock. Still merge it in — show “out of stock” badge. Don’t silently drop items.


Deep Dive 4: Cart Abandonment Detection + Recovery

Problem: 70% of carts are abandoned. Can we nudge users back?

Bad: Poll the DB every hour for “carts not checked out in 24h.” At 50M carts, this is expensive and wasteful.

Good: Event-driven. Kafka consumer tracks item_added events. If no checkout_initiated event arrives within 24 hours for that userId, emit an abandoned_cart event. Notification service sends push/email: “You left items in your cart.”

Great: Tiered nudges:

Implementation: Use a delayed Kafka consumer (or Redis sorted set with fire-time) to schedule nudge events at each tier. Cancel all pending nudges if user checks out.


Deep Dive 5: Inventory Reservation at Checkout

Problem: User has 2 iPhones in cart. They click “Proceed to Checkout.” Between clicking and completing payment (2-5 minutes), another user could buy the last iPhone. How do we prevent overselling?

Bad: Decrement inventory on add-to-cart. 500K carts hold 500K “reservations” but 70% abandon. Real customers can’t buy because inventory is “held” by abandoned carts.

Good: Reserve inventory only at checkout initiation (when user clicks “Pay”). Use a TTL-based hold (10 minutes). If payment isn’t completed, hold releases automatically.

Great: Conditional write with fencing:

DynamoDB:
  UpdateItem(itemId=SKU_123)
  SET available = available - 2
  CONDITION: available >= 2

If condition fails (not enough stock), return “only X left, reduce quantity.” The conditional write is atomic — no two users can over-decrement.

TTL-based hold: write a reservation record {reservationId, itemId, qty, expiresAt: now+10min}. A sweeper restores inventory for expired reservations. Same pattern as BookMyShow seat locks.


8. Final Architecture

flowchart TD
    subgraph Clients
        WEB["Web Browser"]:::client
        APP["Mobile App"]:::client
    end

    subgraph Edge
        GW["API Gateway<br/>rate limit + auth"]:::edge
    end

    subgraph Services
        CS["Cart Service"]:::service
        INV["Inventory Service"]:::service
        PRICE["Pricing Service"]:::service
        CAT["Catalog Service"]:::service
        MERGE["Merge Handler"]:::service
        NOTIF["Notification Service"]:::service
    end

    subgraph Data
        REDIS[("Redis Cluster<br/>hot carts")]:::data
        DDB[("DynamoDB<br/>durable carts")]:::data
        PG[("Postgres<br/>inventory")]:::data
    end

    subgraph Async
        KF["Kafka"]:::async
    end

    WEB --> GW
    APP --> GW
    GW --> CS
    CS --> REDIS
    CS -.-> KF
    KF -.-> DDB
    KF -.-> NOTIF
    CS --> INV
    CS --> PRICE
    CS --> CAT
    CS --> MERGE
    INV --> PG
    MERGE --> REDIS

    classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
    classDef edge fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
    classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
    classDef async fill:#3b1f5e,stroke:#c084fc,color:#e2e8f0

How it works end-to-end:

  1. User adds item → API Gateway authenticates and rate-limits → Cart Service writes to Redis (sub-ms)
  2. Cart event published to Kafka → consumed by DynamoDB writer (durability) and analytics pipeline
  3. User views cart → Cart Service reads from Redis + enriches in parallel from Inventory, Pricing, and Catalog services
  4. User logs in → Merge Handler combines guest cart with user cart, writes merged result to Redis
  5. User proceeds to checkout → Inventory Service reserves stock with conditional write (DynamoDB or Postgres row lock)
  6. If user abandons → Kafka-based timer fires nudge notifications at 2h, 24h, 72h intervals
  7. If Redis fails → Cart Service falls back to DynamoDB reads (slightly slower but no data loss)

Metrics to Monitor

Business Metrics

Metric What it tells you
Cart abandonment rate % of carts that never reach checkout
Cart-to-checkout conversion Funnel health
Avg items per cart Engagement signal
Time: add-to-cart → checkout Are users hesitating?
Price-change impact How many users see “price increased” and remove item?

Technical Metrics

Metric Alert threshold
Add-to-cart P99 latency >100ms
Cart read P99 latency >200ms
Redis hit rate <95%
Redis → DynamoDB sync lag >5 seconds
Inventory service P99 >50ms
Cart merge error rate >0.5%
Flash sale counter (carts holding item) Track for demand forecasting

What’s Expected at Each Level

Mid-level

Design a basic cart with add/remove/get operations using Redis. Recognize the need for persistence beyond Redis. Propose DynamoDB or Postgres as backup. Understand why inventory shouldn’t be checked at add-time.

Senior

Propose the write-behind pattern (Redis + Kafka + DynamoDB). Discuss cart merge strategies. Explain flash sale handling without blocking the add-to-cart path. Design the enrichment layer with parallel calls and graceful degradation. Discuss the checkout reservation with conditional writes.

Staff+

Discuss multi-region cart consistency, version vectors for conflict resolution, cart-level rate limiting for bot prevention during flash sales, and the abandoned-cart recovery pipeline with tiered nudges. Quantify costs (Redis cluster sizing, DynamoDB on-demand pricing). Propose observability: which metrics distinguish a healthy cart system from a degraded one.


Key Takeaways



Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.