Problem RestatementProblem
JPMorgan asked two versions:
- Design a scalable e-commerce website: browse products, search, cart, checkout, orders and payment.
- Improve an existing system: the shopping cart (web and mobile) already works, but it's slow, and product wants "notify me when this item is back in stock". You should improve the current design, not rewrite it.
RequirementsRequirements
- Browse and search products, product pages.
- Add to cart, view the cart, update quantities.
- Checkout: address, payment, order creation, inventory update.
- Order history and status.
- Back-in-stock alerts.
- Handle big sale days (5–10x traffic).
Architecture (greenfield)Architecture
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
U["Web / Mobile"] --> CDN["CDN - static, product pages"]
U --> GW["API Gateway"]
GW --> CAT["Catalog"]
GW --> SRCH["Search"]
GW --> CART["Cart Service"]
GW --> CHK["Checkout / Orders"]
CART --> RC[("Redis - carts")]
CART --> CDB[("Cart DB - durable")]
CHK --> INV[("Inventory")]
CHK --> PAY["Payments"]
CHK --> ODB[("Orders DB")]
CHK --> K[("Events")]
K --> NOTIF["Notifications"]
INV --> K- Catalog + Search: read-heavy. Product pages are cached at the CDN, and search uses a search index.
- Cart: fast reads and writes in Redis, persisted to a durable DB (so carts survive restarts and work across devices).
- Checkout/Orders: validates price and stock, reserves inventory, takes payment, creates the order (idempotently).
- Events: order placed, stock changed, feeding notifications and analytics.
2.1 Checkout in short
- Re-validate the cart (prices, availability).
- Reserve inventory atomically (
available >= qty→ decrement and hold for 10 minutes). - Authorize payment → create the order → confirm the reservation. If payment fails, release the hold.
- An idempotency key on "Place order" prevents double orders.
Version 2: Improving an Existing Slow Cart
Step 1: Measure first. Don't guess. Add tracing to the cart endpoints and find where time goes. Typical findings:- The cart page calls many services one by one (product details, price, stock, promotions) for each item → N sequential calls. Fix: batch calls (
getProducts(ids)), make them in parallel, and cache product data. - Every read hits the DB with joins. Fix: keep the cart as one document in Redis (write-through to the DB), and invalidate or update it on each change.
- Price and stock computed live on every view. Fix: cache prices briefly, and show stock as "In stock / Few left" from a cached value, with exact checks only at checkout.
- Chatty mobile clients re-fetching the whole cart after every tap. Fix: return the updated cart in the mutation response, use ETags, and avoid extra round trips.
- Large payloads: send only what the cart UI needs.
Deep Dive — "Notify me when it's back in stock"Deep dive
Fifty thousand people subscribe to a sold-out console. Twenty units arrive. What the system does in the next ten seconds decides whether this feature helps customers or just creates a riot.
Poll inventory for every subscriber
A job walks the subscription list and checks stock for each one.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
SUB["500,000 subscriptions"] --> LOOP["Check stock for each, every few minutes"]
LOOP --> DB[("Inventory service")]
DB --> LOAD["Hundreds of thousands of reads for almost always the same answer"]
LOOP --> LAG["Restock at 10:00:02, noticed at 10:04 - already sold out"]Nearly every check confirms nothing changed, and the one time something does change, the answer is minutes stale. The customer gets an email for an item that sold out before they opened it — worse than no email at all.
Publish a restock event
Inventory emits stock_available(sku) when the count crosses zero, and a consumer notifies everyone subscribed to that SKU.
No polling and no lag; notifications go out within a second of the restock. And now the real problem is visible: fifty thousand notifications land simultaneously, fifty thousand people tap at once, and twenty of them get a console. The other 49,980 experience a checkout that fails under load — and the site gets a traffic spike shaped exactly like a DDoS, aimed at the hottest product page.
Notify in waves, sized to the stock
Treat the notification as an invitation, and hand out only as many as there is stock to justify:
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
RS["Restock - 20 units"] --> W1["Wave 1: notify the first 200 subscribers, FIFO"]
W1 --> HOLD["Each gets a short reservation window"]
HOLD --> BUY{"Units left after the window?"}
BUY -->|"yes"| W2["Wave 2: next 200"]
BUY -->|"no"| STOP["Stop - remaining subscribers keep their subscription"]
W2 --> BUY- Wave size follows the stock, not the subscriber count. Twenty units justify inviting a few hundred people, not fifty thousand.
- Order is the subscription order. People who subscribed first get invited first, which is both fairer and easier to explain than a race.
- A short reservation window per invited customer turns the race into a queue. Unclaimed reservations expire and free the unit for the next wave.
- Un-notified subscribers stay subscribed. Nobody loses their place because the batch ran out.
Two smaller details that matter: deduplicate per user per SKU with a cool-down, so a flapping stock count does not send six emails; and make the notification link carry the reservation, so tapping it goes straight to a checkout that will actually succeed.
Handling Sale Days
- Pre-scale stateless services, warm the caches, and serve static and product pages from the CDN.
- Put a queue or waiting room in front of checkout for extreme spikes.
- Protect inventory with atomic decrements, and use a separate hot-item stock counter (e.g., Redis) for flash-sale items, reconciled with the DB.
Trade-offs & AlternativesTrade-offs
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Cart storage | Redis + durable DB | Fast and safe | DB only: slow; cache only: carts lost |
| Stock on cart page | Cached approximate status | Fast | Exact live stock: slow, not needed until checkout |
| Checkout | Reserve → pay → confirm, idempotent | No oversell, no double orders | Decrement after payment: oversell risk |
| Back in stock | Event on 0→positive, batched sends | Efficient, no spam | Poll stock for every subscriber: wasteful |
Wrap-UpWrap-up
Split the site into catalog and search (CDN and cache heavy), cart (Redis + durable DB), and checkout (re-validate, reserve stock atomically, pay, create the order idempotently), connected by events. To fix a slow existing cart, measure with tracing, then batch and parallelize service calls, cache the cart and product data, and trim payloads, rolling out behind a flag. Implement back-in-stock alerts as subscriptions triggered by a stock 0→positive event, sent in batches.