Shopping Cart System Design Interview: The 45-Minute Walkthrough

June 11, 202612 min read
interview-prepcareersystem-designalgorithms
Shopping Cart System Design Interview: The 45-Minute Walkthrough
TL;DR
  • Guest carts live in Redis keyed by session ID with a 7-day TTL; no database write needed since guests have no expectation of permanence
  • Authenticated carts use the same Redis structure but get async write-through to PostgreSQL so they survive Redis eviction or restart
  • Cart merge on login must be additive, summing quantities for shared items, and wrapped in a Lua script to prevent double-merging from retries
  • Inventory is checked at checkout, not at add time; reserving stock on add causes phantom unavailability because 70% of carts never reach checkout
  • Price snapshot is stored at add for display only; checkout always re-fetches the live price before charging so stale prices never reach payment
  • Five API endpoints cover all cart operations; routing resolves both guest and authenticated paths to a cart key string before any business logic runs

A shopping cart sounds like the easiest service in e-commerce. It is a list. You add things, remove things, buy things. Most candidates fire up their diagramming tool, draw one box labeled "DB", and consider the architecture done.

Then the interviewer says: "What happens when a guest adds items and then logs in?"

That one question opens three storage decisions, a merge algorithm, and a race condition at checkout. The cart is not a list. It is a session artifact that has to become a persistent record at exactly the right moment. Here is how to walk through this shopping cart system design interview cleanly in 45 minutes.

Clarify Before You Draw

Five minutes of requirements saves twenty minutes of redrawing. Seriously. Ask these questions now and you will not have to demolish half your diagram at minute 30.

Functional requirements:

  • Guests and authenticated users can add, update, remove, and view items
  • Cart persists across browser sessions for logged-in users
  • Guest cart merges into the user cart on login
  • Checkout hands cart contents to an order service and returns an order ID

Non-functional requirements:

  • Cart reads and writes under 50ms p99
  • 1 million concurrent active carts (mid-size e-commerce, not Amazon Day 1)
  • Cart data is soft-critical: losing a guest cart is annoying, losing a paid order is catastrophic
  • No inventory reservation at add time (always confirm this with the interviewer; it shapes everything)

The inventory question is the most important clarification. If your interviewer says yes, reserve stock on add, your design changes dramatically. The almost-universal answer is no, and the reason is cart abandonment: roughly 70% of carts never reach checkout. Locking inventory for every one of those would cause phantom unavailability across your whole catalog. Popular items would show as sold out while the actual stock sat trapped inside carts that got abandoned at the "create account" screen.

What You're Actually Building

The core tension is this: a shopping cart is read-heavy, write-frequent, can tolerate eventual consistency on the browsing path, but must be strongly consistent at checkout. Those two modes need different treatment.

System architecture: Client to Cart Service, then to Redis (hot path), PostgreSQL (async write-through), Product Service, and Order Service on checkout

The Cart Service owns exactly one thing: the mapping from a user (or session) to a list of product IDs and quantities. Everything else is someone else's problem.

The Cart Service owns exactly that one thing. It does not own product catalog data. It does not own inventory counts. It does not own orders. Scope creep past that boundary is the most common mistake in this interview, and interviewers notice it immediately.

Scale math: 1 million concurrent carts, average 5 items, around 200 bytes per cart. That is roughly 200MB of hot cart state. Redis handles it in its sleep. For 10 million users with carts over a 30-day window, you are still well under 2GB of Redis memory. A single Redis node suffices at moderate scale; shard by user ID hash when you need to grow.

Guest and User Carts Are Not the Same Problem

Guest users and authenticated users have fundamentally different cart lifecycles, and this fork is where most designs go wrong by treating them identically. The "one carts table" approach works fine until your first user actually tries to log in.

Guest carts live in Redis keyed by session ID: cart:session:{sessionId}. The session ID goes into a cookie. No database write. Set a 7-day TTL. If the cart expires, it is gone. This is acceptable: the guest never authenticated, never gave you an email, and has no expectation of permanence. They are a browser with a cookie. That is the whole relationship.

Authenticated carts also live in Redis, cart:user:{userId}, but they need a write-through to PostgreSQL. Redis is your cache. The database is your source of truth. An async background write after each Redis update keeps them in sync without adding latency to the API response.

Use a Redis Hash for each cart. The key is the cart identifier. Each field is a product ID. Each value stores quantity alongside a price snapshot.

HSET cart:user:42  prod:101  '{"qty": 2, "priceAtAdd": 29.99}'
HSET cart:user:42  prod:207  '{"qty": 1, "priceAtAdd": 14.99}'
EXPIRE cart:user:42 2592000  # 30 days

This gives you O(1) add, update, and remove, plus full cart retrieval with a single HGETALL. A Redis Cluster sharded by user ID scales this horizontally without touching the data model.

Redis Hash layout: each cart is one key, each product is a field, each value is a JSON string with qty and priceAtAdd

One key per cart. One field per product. HGETALL returns the whole thing in a single round-trip. That is the entire data model.

Guest Login Breaks the Simple Design

When a guest authenticates, you have two carts in Redis. The naïve fix is to delete the guest cart and load the user cart. This silently throws away everything the guest added while browsing. Users notice. They wrote about it on Twitter in 2009 and they are still writing about it now.

The correct approach is additive merge: for each item in the guest cart, increment its quantity in the user cart. If the user already has 1 of product A and the guest cart has 2, the merged result is 3. Items the user does not have yet are added whole.

Additive cart merge: guest cart (A×2, B×1) and user cart (A×1, C×3) feed into a Lua EVAL script and produce a merged cart (A×3, B×1, C×3) with the guest key deleted atomically

Additive merge inside a Lua script. Guest cart gets deleted inside the same atomic operation. Double-login from a second tab cannot merge the same cart twice.

Do the merge inside a Lua script to make it atomic. This prevents a second login request (from another tab or a retry) from merging the same guest cart twice and doubling every quantity. That is the kind of bug that generates a lot of very confused customer support tickets.

# Cart merge pseudocode def merge_carts(guest_session_id: str, user_id: str): guest_key = f"cart:session:{guest_session_id}" user_key = f"cart:user:{user_id}" guest_items = redis.hgetall(guest_key) for product_id, raw in guest_items.items(): guest_item = json.loads(raw) existing = redis.hget(user_key, product_id) if existing: merged_qty = json.loads(existing)["qty"] + guest_item["qty"] redis.hset(user_key, product_id, json.dumps({ "qty": merged_qty, "priceAtAdd": guest_item["priceAtAdd"] })) else: redis.hset(user_key, product_id, raw) redis.delete(guest_key)

In production, wrap this in a Lua script executed via EVAL so the read-modify-write sequence cannot be interleaved.

PostgreSQL Is Your Recovery Path, Not Your Cache

The persistent schema mirrors the Redis structure and serves as the recovery path if Redis loses data. It is not the hot path. If you are reaching Postgres for routine cart reads, something has gone wrong.

carts ( cart_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES users(user_id), created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now() ); cart_items ( cart_id uuid REFERENCES carts(cart_id) ON DELETE CASCADE, product_id uuid NOT NULL, quantity int NOT NULL CHECK (quantity > 0), price_snapshot numeric(10,2) NOT NULL, added_at timestamptz DEFAULT now(), PRIMARY KEY (cart_id, product_id) );

Write to PostgreSQL asynchronously after each Redis update via a background worker or event queue. The API does not wait for the DB write to respond to the client. Redis answers. Postgres catches up.

Five Endpoints Is All You Need

GET    /cart                    # return full cart (by userId or sessionId)
POST   /cart/items              # body: {productId, quantity}
PUT    /cart/items/{productId}  # body: {quantity}
DELETE /cart/items/{productId}  # remove one item
POST   /cart/checkout           # validate, hand off to Order Service

The service reads userId from the auth token when present and falls back to sessionId from a cookie. That single routing decision at the request layer keeps every downstream function clean because there is no user/guest branching inside business logic. Both paths resolve to a cart key string. After that point, the code does not care whether it is talking to a guest or an authenticated user.

When Does Inventory Enter the Picture?

Not at add time. At checkout time.

If you reserve inventory when an item hits the cart, popular items appear sold out while the actual stock sits locked inside abandoned carts. At 70% abandonment rates, this is not a theoretical problem. You would effectively lie to customers about availability for most of the day, every day.

Check and reserve inventory at POST /cart/checkout, not before. The Cart Service calls the Inventory Service to verify stock for each line item. If any item is unavailable, checkout returns a structured error listing exactly which items failed. The customer sees the problem before payment is requested. That ordering matters: do not charge someone and then tell them the item is gone.

POST /cart/checkout sequence: Cart Service calls Inventory Service (reserve all items). Success path goes to Order Service then Payment Service. Failure path returns 409 with failed items listed before payment is ever touched.

Inventory failure returns a 409 before the Payment Service is ever called. The customer finds out their item is unavailable, not their bank.

Handle stale cart display with a soft check: annotate cart items with current availability on GET /cart using a read from a product availability cache. Warn the user; do not block the add operation.

The Price in the Cart Is Wrong at Checkout

Store the catalog price at add time as priceAtAdd in Redis and in PostgreSQL. It is useful for display. It is not authoritative. The snapshot is for display only. Checkout always re-fetches the authoritative price from the pricing service before charging. Show the user any differences so they are not surprised at payment.

This catches price increases (which you must honor) and price drops (which you should pass through as a UX win). Never charge a stale price without validation. This is the kind of thing that gets a company on the front page of Hacker News for the wrong reasons.

How to Pace Your System Design Interview Answer

Minute rangeFocus
0-5Requirements: functional, non-functional, guest vs auth, inventory timing
5-15High-level architecture: Cart Service, Redis, PostgreSQL, downstream services
15-25Data model (Redis Hash structure, SQL schema) and API surface
25-35Guest cart merge and checkout flow with inventory check
35-42Scaling: Redis cluster sharding by user ID, async write-through, TTL decisions
42-45Tradeoffs: client-side vs server-side, merge strategy, price consistency

Hit the merge problem around minute 25. That moment separates candidates who thought about real user flows from those who designed for the happy path only. An interviewer who has seen hundreds of these interviews has a very good memory for who brought up guest login on their own.

The Tradeoffs That Decide Everything

Redis-only vs Redis plus database. Redis-only is simpler and fully in-memory, but you lose authenticated carts on eviction or node restart. The hybrid approach adds operational complexity but provides durable carts for logged-in users. Match the choice to your durability SLA. A flash sale platform with millions of active checkout sessions treats Redis eviction very differently than a B2B catalog site.

Client-side cart vs server-side cart. Storing the cart in localStorage eliminates the server entirely for reads. It breaks down when users switch devices or when server-side pricing logic needs to run before checkout. Most real systems start client-side and migrate server-side when cross-device sync becomes a requirement. Both answers are defensible. Pick one and explain why.

Additive merge vs last-write-wins. Additive merge is the most user-friendly strategy but can produce absurd quantities if the merge fires twice. A Lua script with idempotency (check whether the guest cart key still exists before merging, delete it atomically at the end) solves the double-merge case.

Inventory reservation window. Some systems do reserve inventory at checkout initiation for a short window (five to fifteen minutes) to prevent the item going out of stock between checkout start and payment confirmation. This is a valid enhancement once basic checkout works, not a first-pass design decision. Mention it as a follow-up. Do not try to design it in your first pass.

For related problems in the system design space, see the distributed cache system design guide for a deeper treatment of Redis consistency patterns, the e-commerce system design walkthrough for the broader platform context this service lives in, and the payment system design interview guide for the service that takes over after POST /cart/checkout returns.

If you want to practice walking through this under real interview pressure, SpaceComplexity runs voice-based system design mock interviews with rubric-based feedback. The shopping cart is a favorite problem because it tests breadth in one session: caching, session management, service boundaries, and consistency modes all show up before minute 30.

Recap

  • Guest carts in Redis with 7-day TTL; authenticated carts in Redis with async write-through to PostgreSQL
  • Redis Hash per cart: cart:{type}:{id}, field per product, JSON value with quantity and price snapshot
  • Merge on login: additive, atomic via Lua script, delete guest cart inside the same transaction
  • Inventory checked at checkout, not at add; annotate the browsing path with soft availability warnings
  • Five endpoints; routing resolves to a cart key string before any business logic runs
  • Price snapshot for display; live price re-fetch at checkout before charging

Further Reading