Marketplace & Booking
Design Shopping Cart
01
What the cart must do
What the cart must do
- Add an item (a specific SKU, so size and colour are already chosen), change its quantity, remove it, or move it to a saved-for-later list.
- Work for guests with no account, using a cart ID in a cookie, and for signed-in users across phone, laptop and app.
- When a guest signs in, merge the guest cart into the account cart without losing or doubling items.
- Show current prices, applied promotions, estimated tax and shipping, and flag anything whose price or stock has changed since it was added.
- Hand a consistent snapshot of the cart to checkout, which reserves stock and creates the order.
- Emit events for abandoned-cart reminders and for analytics.
- Add to cart answers in p99 under 150 ms; viewing the cart, including repricing, under 300 ms.
- An acknowledged add is never lost, even if a node fails a second later.
- Stay writable at 99.95% during sales, when traffic is ten times normal.
- A user editing from two devices sees one cart, and edits made a moment apart both survive.
- Never charge a price the shopper was not shown at the final review step.
Ask whether adding to the cart should hold stock. For a normal retailer the answer is no, and that one decision removes a huge amount of complexity. Also ask how long guest carts live, whether there is a per-item quantity limit, and whether the cart has to work across country storefronts with different currencies.
02
Sizing the load
Sizing the load
Assume a large marketplace with 120 million monthly shoppers, of whom 25 million touch their cart on a normal day. A typical session makes about nine cart changes and views the cart about five times per change. Sale days run at ten times the normal peak.
| Quantity | Working | Result |
|---|---|---|
| Shoppers who edit a cart per day | assumption | 25M |
| Cart writes per day | 25M x 9 changes | 225M |
| Average write rate | 225M / 86,400 s | about 2,600 / s |
| Sale-day peak writes | 10x average, concentrated in the first hour | about 26,000 / s |
| Cart reads per day | 5 reads per write, badge plus cart page | about 1.1B |
| Sale-day peak reads | 10x average read rate | about 130,000 / s |
| Carts kept | 90 days for accounts, 30 days for guests | about 300M |
| Size of one cart record | 6 lines on average x 180 B, plus header and metadata | about 1.5 KB |
| Cart store size | 300M x 1.5 KB, then 3 replicas | 450 GB raw, about 1.4 TB stored |
| Repricing calls on read | 130K reads / s x 6 lines, batched per cart | about 130,000 batched calls / s |
The data is small: well under 2 TB fits comfortably in any partitioned key-value store. The pressure comes from repricing on every read and from sale-day spikes, where a single popular product can appear in millions of carts in the same ten minutes.
The cart badge in the header only needs a count. Store the item count in a signed cookie updated on every write, and the most common read never touches the backend.
03
API design
API design
Two details are worth pointing out. Writes carry an idempotency key, because mobile networks retry and a double tap should not mean two pairs of shoes. And the priced view returns a quote ID, so checkout can prove which prices the shopper saw.
04
Architecture overview
Architecture overview
flowchart LR
C["Web and app clients"] --> GW["API gateway"]
GW --> CS["Cart service"]
CS --> DB[("Cart store, KV by cart_id")]
CS --> PR["Pricing and promotions"]
CS --> INV["Inventory read API"]
CS --> ID["Identity service"]
CS -. "cart events" .-> K[["Event stream"]]
K --> AB["Abandoned cart worker"]
K --> AN[("Analytics")]
GW --> CO["Checkout service"]
CO --> CS
CO --> RES["Stock reservation"]
CO --> ORD[("Orders")]The cart service is stateless and owns exactly one thing: the list of lines a shopper intends to buy. It does not own prices, stock or orders. Every cart lives as one record in a key-value store keyed by cart_id. Account carts and guest carts share the same table; a separate small index maps user_id to the user's active cart ID.
Reads are where the work happens. When the shopper opens the cart page, the service loads the record, then makes one batched call to pricing for all lines and one to the inventory read API for availability. It compares current prices against the price each line was added at and attaches notices. If pricing is slow, it returns the cart with last known prices and a flag rather than blocking the page.
Checkout is a separate service that asks the cart for a snapshot at a given version, reserves stock, takes payment and writes the order. Splitting them keeps the cart simple and lets checkout be the place where correctness is enforced. Every cart change also goes onto an event stream, which drives abandoned-cart emails, recommendations and funnel analytics without touching the request path.
DynamoDB, Cassandra or a sharded Postgres all work. You need single-digit-millisecond reads by key, conditional writes on a version number, and per-item TTL to expire old guest carts.
Owned by another team. Our contract is a batch endpoint that takes a list of SKUs plus quantities and returns line prices and applicable promotions in one round trip.
A cached, slightly stale view of stock good enough to say "only 2 left". It is not the reservation system, which checkout calls directly.
Kafka or Kinesis. Consumers are allowed to be minutes behind, because nothing on the shopper's screen waits on them.
05
Deep dive: the cart record, concurrent edits, merging and checkout
Deep dive: the cart record, concurrent edits, merging and checkout
Storing a list of SKUs is easy. The interview is about three moments: two edits arriving at the same time, a guest cart meeting an account cart at sign-in, and the handoff to checkout, where prices and stock must be pinned down.
One record per cart
Keep the whole cart in a single record rather than one row per line. A cart rarely has more than a few dozen lines, so the record stays small, every read is one key lookup, and every write can be guarded by a single version number. Row-per-line looks more normalised but turns "show my cart" into a range query and makes atomic multi-line edits, such as a merge, harder.
Two edits at once
A shopper adds a jacket on the phone while the laptop, open in another room, removes a scarf. Both requests read version 15. Without care, whichever write lands second overwrites the first and an item silently reappears or vanishes.
Every write is a conditional put: "replace this record only if version is still 15". The loser gets a conflict, rereads the record, reapplies its own operation to the fresh copy and tries again. Because operations are small and expressed as intents, such as "add one of SKU X" or "set line Y to quantity 0", replaying them on newer state is safe. Conflicts are rare; even on sale days, two writes to the same cart within the same 20 ms are unusual.
Retries from flaky networks are a different problem. The client sends the same idempotency key on a retry, and the record keeps the last 50 applied keys in recent_ops. If the key is already there, the service returns the current cart without reapplying the change.
Merging at sign-in
flowchart TD L["Guest signs in"] --> T["Identity issues merge token"] T --> G["Load guest cart"] T --> A["Load account cart"] G --> M["Combine lines by SKU"] A --> M M --> Q["Cap quantity at per-item limit"] Q --> W["Conditional write to account cart"] W --> X["Mark guest cart merged into account cart"] X --> R["Client switches to account cart_id"]
Merge rules should be boring and predictable. Lines are combined by SKU. If both carts hold the same SKU, take the larger of the two quantities, not the sum: the common case is that the shopper added the same thing on two devices, not that they want twice as many. Respect per-item limits, and move anything that cannot be merged, such as an item from another country storefront, into saved for later with a notice.
The merge must survive retries and crashes halfway through. The identity service issues a merge token at login, and the account cart records which tokens it has already applied. The guest cart is then marked as merged with a pointer to the account cart instead of being deleted, so a stale browser tab still writing to the old cart ID is redirected rather than creating a ghost cart.
Prices, stock and the handoff to checkout
The cart never promises a price. It records the price seen at add time only so it can tell the shopper "this dropped by 300 rupees" or "this went up". The authoritative price comes from pricing on every priced view, and that view returns a quote valid for about fifteen minutes.
Checkout starts from the quote. It reads the cart at the quoted version, asks pricing to confirm the quote is still honoured, and calls the reservation system to place a hold on each line with a short expiry, typically ten minutes. If any line fails, the shopper sees exactly which item changed and why before paying. Only after payment succeeds does the hold become a decrement, and only then is the cart cleared, using the same conditional write on the version checkout started from.
| Moment | Stock | Price |
|---|---|---|
| Add to cart | Advisory read only, no hold | Recorded for change notices |
| View cart | Cached availability, "low stock" hints | Live repricing, quote issued |
| Start checkout | Hold placed for about 10 minutes | Quote confirmed or shopper warned |
| Payment succeeds | Hold converted to a sale | Charged amount equals confirmed quote |
Most carts are never bought. Holding stock for every add would lock up popular items in carts that are abandoned, and you would need a sweeper to release them. Holding only during checkout keeps holds short-lived and few. Flash sales are the exception: there you may hold for a few minutes at add time and accept the operational cost.
06
How to handle the interview
How to handle the interview
Say what the cart does not own. Prices, stock and orders belong to other services. Drawing that boundary early keeps the design small and shows you think in terms of ownership.
Decide on stock holds explicitly. Most interviewers want to hear "no hold on add, a short hold at checkout" and why, with flash sales as the exception.
Use versions for concurrency. Conditional writes on a version number, plus intent-style operations that can be replayed, are simpler than locks and fit a key-value store.
Walk through the guest merge. Max not sum, per-item limits, a merge token for retries, and a tombstone on the guest cart. Each rule prevents a specific bug.
Bring up idempotency before they do. Double taps and retried requests on mobile are the most common cart bug in production.
Connect the quote to the charge. Explain how the price at review is pinned so the shopper is never charged something they did not see.
Keep events off the hot path. Abandoned-cart emails and analytics read a stream. A slow email service must never slow down add to cart.
07
Mistakes to avoid
Mistakes to avoid
An in-memory store with no durable backing loses every cart on a bad restart, usually on the busiest day of the year.
Use a durable store as the source of truth. Add a cache only if measurements show you need one.A price frozen when the item was added drifts from the catalogue, and you end up either overcharging or eating losses.
Record the added price for notices only and reprice on every view and at checkout.Abandoned carts lock up stock and a sweeper has to chase millions of expiring holds.
Hold only during checkout, with a short expiry.A shopper who added one item on two devices suddenly has two in the cart and is annoyed at checkout.
Take the larger quantity and show a notice when lines were combined.Concurrent edits from two tabs silently undo each other.
Use conditional writes and replay the losing operation on fresh state.08
Decisions and what they cost
Decisions and what they cost
Single-key reads and writes, one version number for the whole cart, and natural TTL for expiry. The record is small, so rewriting it on every change is cheap.
Fits the access patternGives SQL for analytics and foreign keys to the catalogue. Every view becomes a multi-row read and atomic multi-line changes need transactions, which is harder to shard.
Fine for one region and modest trafficNo locks, no lock timeouts. Conflicts are rare and resolved by rereading and reapplying the intent. Works the same across devices and regions with a single writer region.
Simple and correctSerialises writes, but a crashed holder blocks the cart until the lock expires, and you have added a second system to the write path that can fail independently.
More moving partsThe shopper always sees current prices and promotions, and checkout charges exactly the confirmed quote. The cost is a pricing call on every cart view.
No surprise chargesCuts pricing load during sales, but prices shown can lag a promotion change. Acceptable as a fallback when pricing is overloaded, not as the default.
Use as degradation modeEach cart is written in one region, chosen by the user's country, and replicated elsewhere for reads and failover. Version checks stay meaningful.
Clear ownershipAn add-wins set per SKU can merge edits from two regions without coordination. Removals and quantity changes get tricky, and the extra complexity rarely pays off for carts.
Overkill for most stores09
Failure modes
Failure modes
The promotions engine falls over under sale traffic. Every cart view waits on it, cart pages time out, and shoppers think the site is broken even though their carts are fine.
Set a tight timeout on the pricing call, return the cart with last known prices and a "prices are being updated" banner, and block only the final checkout step until a confirmed quote is available.A shopper on a slow train taps "Place order" twice. Both requests start checkout from the same cart version, and without protection both charge the card.
Checkout takes an idempotency key derived from the cart ID and version. The second request finds the first in progress and returns its result. Clearing the cart uses a conditional write on that same version.A popular item sells out while ten thousand people have it in their carts. They all reach checkout at once and the reservation system rejects most of them.
Show stock warnings on the cart page from the cached inventory view, fail the reservation per line rather than for the whole order, and offer to move the missing item to saved for later so the rest of the order can go through.The login response is lost and the client retries the merge. Without protection, the guest lines are applied again, or the second run finds the guest cart already empty and wipes lines in the account cart.
Merge tokens recorded on the account cart make the second run a no-op. The guest cart is tombstoned with a pointer, never emptied in place.Scalper bots add thousands of units of a limited item to hundreds of carts, hoping holds will lock it up.
Because adds do not hold stock, the bots gain little. Enforce per-item and per-cart quantity limits, rate-limit adds per account and device, and reserve only at checkout, where stronger bot checks run.The primary region for Indian shoppers has an outage. Carts there cannot be written.
Promote the replica in a second region. Recent writes may be a few seconds behind, so clients resend their last operation with its idempotency key, which is applied once and ignored if it already made it across.10
Growing the design in stages
Growing the design in stages
Start with the cart as a table in the main database. Pull it out into its own service when either its traffic or its release schedule starts hurting the rest of the store.
A cart_lines table next to products and orders, a session cookie for guests and a simple merge on login. One Postgres handles it, and that is the right call for a new store.
The cart moves behind its own API with a document per cart, versioned writes and idempotency keys. Pricing becomes a service call instead of a join.
The cart store moves to a partitioned key-value database. Cart views fall back to cached prices when pricing is slow, and the cookie badge takes the most common read off the backend.
Checkout starts from a pinned quote and places ten-minute holds. Cart events go to a stream that drives reminders and analytics, so the cart stops calling other systems synchronously.
Carts are written in the region that serves the shopper and replicated elsewhere for failover. Cross-storefront items are no longer merged blindly, because currency and tax rules differ.
Watch and read
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.