Marketplace & Booking
Design Flash Sale
01
Requirements
Requirements
- Admin creates a flash sale event: product, quantity, start time, duration
- Users enter a waiting room before sale starts; admitted in controlled batches at T=0
- Admitted user sees product page with live inventory count → clicks Buy → instant checkout
- Payment pre-authorized during queue wait; charge on successful purchase only
- Fair ordering: FIFO within the waiting room; no advantage to page-refresh spam
- Sold-out notification to remaining queued users immediately
- Zero oversell — never sell item #1001 of 1000
- Checkout latency < 500 ms for admitted users
- Waiting room absorbs 100K+ concurrent connections
- Sale start within < 1 sec of configured time (clock-accurate)
- Survive bot attacks: CAPTCHA + rate-limit + proof-of-work
- Graceful: sold-out ≠ error; it's a clean UX state
02
Scale Estimation
Scale Estimation
03
API Design
API Design
User enters waiting room. Returns {queue_ticket, position, estimated_wait_sec}. Ticket is a signed JWT with enqueue timestamp — proves FIFO position. Requires CAPTCHA token.
Poll queue status. Returns {status: waiting|admitted|sold_out, position, inventory_remaining}. Client polls every 2 s or receives SSE push.
Admitted user purchases. Body: {ticket, payment_token, quantity: 1}. Atomic inventory decrement + payment capture. Returns {order_id, status: success|sold_out}. Idempotency via ticket — can't buy twice.
Pre-authorize payment while in queue. Returns {auth_id}. If user doesn't win, auth voided automatically after 30 min.
Live inventory count. Served from Redis; eventually consistent (~1 s lag). Used for countdown UI.
04
Architecture
Architecture
Three tiers isolated by concern: queue tier (absorb + order the herd), checkout tier (atomic purchase), payment tier (pre-auth + capture). A CDN serves the static product page; only the queue-enter and purchase calls hit the backend.
05
Deep Dive — Atomic Inventory + Waiting Room
Deep Dive — Atomic Inventory + Waiting Room
The single hardest problem: 100K users all call POST /purchase in 1 second. If each checks inventory, sees "999 remaining," and then decrements — you sell 100K items instead of 1K. Classic lost-update / check-then-act race.
Solution: Redis Lua atomic decrement.
-- Redis Lua script: atomic decrement-if-positive
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
redis.call('DECR', KEYS[1])
return 1 -- success
else
return 0 -- sold out
end
This runs atomically inside Redis (single-threaded). No race. No oversell. Each call either gets a unit or doesn't. The caller gets a definitive answer in < 1 ms.
Waiting room. Without it, 100K users slam the checkout tier simultaneously — even if inventory check is atomic, the backend can't handle 100K DB writes/sec. Solution: a queue that controls admission rate.
- User enters queue. Redis ZADD with score = enqueue timestamp. Returns ticket (signed JWT) + position.
- Admission controller (a cron or timer) pops the top N users per second (e.g., 500/sec) from the sorted set. Marks them "admitted" in Redis.
- Admitted user's next poll returns status=admitted. Client-side JS transitions to checkout page.
- User calls
POST /purchasewith ticket. Checkout service verifies ticket is admitted, calls Redis Lua decrement, on success writes order to Postgres + captures pre-authed payment. - When inventory hits 0, admission controller stops admitting. Kafka event fires. SSE/WS pushes "sold out" to all remaining queue connections.
sequenceDiagram
participant U as User
participant Q as Queue svc
participant AC as Admission ctrl
participant C as Checkout svc
participant R as Redis (inventory)
participant P as Payment svc
U->>Q: POST /enter-queue (CAPTCHA)
Q-->>U: ticket + position=4521
U->>Q: GET /status (poll)
Q-->>U: waiting, position=312
AC->>Q: pop top 500 from ZSET
Q-->>AC: [user_tokens...]
AC->>Q: mark admitted
U->>Q: GET /status
Q-->>U: admitted
U->>C: POST /purchase (ticket)
C->>R: Lua DECR-if-positive
R-->>C: 1 (success)
C->>P: capture pre-auth
P-->>C: captured
C-->>U: order confirmed
Pre-authorization. While user is in queue, client silently calls POST /pre-auth with their saved card. If they win a slot, capture is instant — no "enter your card" delay at checkout. If they lose, auth auto-voids after 30 min. UX: "You're in line. We'll charge your card only if you get one."
"CDN serves the product page. Users enter a FIFO waiting room (Redis sorted set by enqueue timestamp). An admission controller drains N users/sec into checkout. Inventory is a single Redis key decremented atomically via a Lua script — no oversell possible. Payment is pre-authorized during queue wait; on admission + successful decrement, payment captured instantly. When inventory hits 0, 'sold out' pushed to all remaining queue users via SSE. Total purchase latency for admitted user: < 500 ms."
06
Anti-patterns
Anti-patterns
Classic check-then-act race. 100K users all see stock=999 simultaneously; all decrement. You sell 100K items.
No server handles 100K concurrent DB writes/sec gracefully. You get 503s, timeouts, and angry customers.
User takes 30 seconds to type card number. Slot is wasted. Others in queue wait longer. Conversion drops.
07
Tradeoffs & Design Choices
Tradeoffs & Design Choices
- Redis atomic vs Postgres FOR UPDATE. Redis Lua: ~50K ops/sec on single key, sub-ms. Postgres pessimistic lock: ~5K/sec, 2–5 ms. Redis wins overwhelmingly for the hot path. Postgres is the authoritative ledger written after Redis succeeds.
- Waiting room (fair, controlled) vs first-come-first-served (unfair, chaotic). Without a queue, users with lower latency (closer server, faster network) win. Queue equalizes: everyone gets a ticket; admission is FIFO. Supreme and Nike SNKRS both adopted queues for this reason.
- Per-item vs per-SKU inventory. Flash sales typically have one SKU. Single Redis key works. Multi-SKU (sizes, colors) needs one key per variant — same Lua pattern, multiple keys.
- Optimistic (check-and-set) vs pessimistic (lock). At 100K contenders, optimistic = 99K retries. Pessimistic = sequential. Redis Lua is effectively "pessimistic within a single thread" — no retries, no contention, because there's only one thread.
- Pre-auth cost. Pre-authorizing 100K cards when only 1K win = 99K void auths. Payment processors charge ~$0.01 per auth. 100K × $0.01 = $1K per sale — acceptable for high-value items; questionable for $5 items.
08
Failure Modes
Failure Modes
09
Interview Tips
Interview Tips
- Lead with the waiting room. "100K users → 500/sec admission rate → smooth backend load." This is the insight that separates good from great.
- Redis Lua for atomic inventory. Name it explicitly — "single Redis key, Lua DECR-if-positive, no race." Shows you've actually built these systems.
- Pre-auth payment in queue. This UX detail (checkout = one click, not "enter card") is the kind of production nuance interviewers love.
- Bot mitigation is load-bearing. Without it, bots take all 1K items. CAPTCHA + proof-of-work + device fingerprinting. Don't skip this.
- Distinguish from Ticketmaster. Ticketmaster = seat selection + hold pattern. Flash sale = single SKU + atomic counter. Simpler inventory model, harder contention model.
10
Evolution
Evolution
MVP — Postgres UPDATE with row lock
Single DB, UPDATE stock = stock - 1 WHERE stock > 0. Works to ~100 concurrent users. Falls over at 1K.
Redis atomic counter + DB sync
Redis DECR for the hot path. Async write confirmed orders to Postgres. Handles ~10K/sec.
Waiting room + admission control
Queue absorbs the herd. Controlled drain rate keeps backend healthy. Fair FIFO ordering.
Pre-auth + one-click checkout
Payment authorized during queue wait. Winners checkout in < 500 ms. Losers' auths auto-void.
Bot mitigation + regional queues
CAPTCHA + proof-of-work + device fingerprinting. Per-region queue shards for global sales (Xiaomi India vs Xiaomi China).
Watch and read
References & Videos
Try next
Free to read · better with Enzo
Whiteboard this with Enzo
Enzo runs it as a live system design round on the whiteboard and grades your trade-offs.