The Flash Sale Problem
A flash sale offers a limited quantity of items at a steep discount for a short time. The challenge: millions of users simultaneously attempt to purchase a few thousand units. This creates extreme write contention on inventory records — every request wants to decrement the same counter. The naive approach (decrement in the database for each request) leads to database overload, deadlocks, and incorrect inventory counts. Well-known examples: Amazon Lightning Deals, Alibaba’s Double 11 (Singles’ Day), Nike SNKRS launches.
Architecture Overview
The key insight: separate the traffic spike from the actual purchase processing. Layer 1 — Pre-sale validation (CDN/edge): serve the flash sale page from the CDN. Only allow purchase attempts after the sale start time (enforced at the edge). Layer 2 — Token gate (Redis): limit access to the purchase flow using a virtual waiting room or token bucket. Layer 3 — Inventory gate (Redis): check and reserve inventory atomically in Redis before writing to the database. Layer 4 — Order processing (async queue): confirmed inventory holders enter a queue; a worker processes orders at a sustainable rate. Layer 5 — Database (source of truth): the final order and inventory update are persisted asynchronously.
Inventory in Redis (Fast Gate)
Before the sale: pre-load the available quantity into Redis. SET flash:{sale_id}:inventory 10000. On each purchase attempt: use a Lua script for atomic check-and-decrement (Lua scripts run atomically in Redis — no race conditions):
-- Lua script: atomic check-and-decrement
local key = KEYS[1]
local qty = tonumber(ARGV[1])
local current = tonumber(redis.call('GET', key))
if current == nil or current < qty then
return -1 -- insufficient inventory
end
return redis.call('DECRBY', key, qty)
If the script returns -1: reject the request immediately (inventory exhausted). If it returns the new count: proceed to order creation. This handles 100,000+ requests per second with sub-millisecond latency. Redis single-threaded execution of Lua guarantees atomicity without external locking.
Virtual Waiting Room
To prevent the purchase flow from being overwhelmed: at sale start, instead of letting all users hit the checkout simultaneously, redirect them to a waiting room. Each user is assigned a random position in the queue (shuffle on arrival — prevents bots from gaining advantage by arriving first). Every N seconds, admit the next batch of users to the checkout flow. Implementation: Redis sorted set with score = random float (not timestamp). ZADD waitroom:{sale_id} RANDOM_SCORE user_id. Admit users: ZPOPMIN waitroom:{sale_id} COUNT N every T seconds. The admitted users receive a time-limited session token (30 minutes to complete checkout). Token gates access to the checkout page. Without a valid token, checkout redirects back to the waiting room.
Async Order Processing
Users who pass the Redis inventory gate receive an order reference number. Their purchase request is placed in a Kafka queue. Workers consume from the queue and finalize the order: validate payment, decrement database inventory, create order records. This decouples the burst of incoming purchase requests from the database write throughput. Workers process at a steady rate (e.g., 1,000 orders/second). Back-pressure: if the Kafka queue grows beyond a threshold, admit fewer users from the waiting room. Database inventory update: UPDATE inventory SET quantity = quantity – 1 WHERE sale_id = :id AND quantity > 0. If 0 rows updated (database inventory reached 0 before Redis, due to Redis pre-loading slightly more): mark the order as failed, refund the customer, send an apology notification.
Preventing Oversell
Three layers prevent oversell: (1) Redis Lua atomic decrement (fast gate — rejects when Redis counter reaches 0). (2) Database conditional update (quantity > 0 check) — final gate even if Redis and DB drift slightly. (3) Pre-load Redis with slightly fewer units than available (e.g., 9,800 of 10,000) to create a buffer for any Redis-DB sync discrepancy. Monitoring: alert if database inventory goes negative (bug indicator). Track the Redis-DB delta after each sale to tune the pre-load factor.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “Why is a Lua script required for atomic inventory decrement in Redis?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Without atomicity, a check-then-decrement operation has a race condition: two requests both read inventory=1, both check “1 >= 1” passes, both decrement — inventory reaches -1 (oversell). A standard Redis transaction (MULTI/EXEC) provides optimistic concurrency but requires a WATCH command and client-side retry logic, which is complex and has lower throughput under high contention. A Lua script runs as a single atomic operation in Redis’s single-threaded execution model: no other command can execute between the check and the decrement within the script. All clients sending the same Lua script will be serialized by Redis — one at a time. This gives exactly the “check-and-act” atomicity needed for inventory management, without external locking or client retries.”
}
},
{
“@type”: “Question”,
“name”: “How does a virtual waiting room prevent the checkout system from being overwhelmed?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Without a waiting room: all 1 million users hit the checkout flow simultaneously at sale start. Each checkout request involves database reads, Redis lookups, and payment pre-authorization — perhaps 50ms each. 1M concurrent requests at 50ms = 20,000 requests/second. This overwhelms the database and payment service. With a virtual waiting room: users are queued and admitted to checkout in batches of N every T seconds, where N*T = sustainable checkout throughput (e.g., 500 users admitted every 0.5 seconds = 1000/second). The checkout system sees steady load instead of a spike. Users in the waiting room see an estimated wait time and a position indicator. The randomized queue position prevents sophisticated users from gaining advantage by hitting the sale endpoint milliseconds early.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle the case where Redis inventory is exhausted but some database units remain?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Redis is pre-loaded with slightly fewer units than the actual database count (e.g., 9,800 for 10,000 actual units). This intentional buffer absorbs the Redis-DB drift that occurs if Redis counts down to 0 slightly before the database is updated (due to async processing). When Redis reaches 0: the sale appears sold out to new customers. Workers processing the Kafka order queue still have 200 units of database inventory to fulfill. If a worker’s database UPDATE returns 0 rows (quantity was already 0): this is the safety net catching the few orders that slipped through the buffer. Mark those orders as failed, issue a full refund immediately, send an apology email with a future discount code. Track the failure rate — if > 0.1%, increase the Redis pre-load buffer.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle a flash sale for multiple products with independent inventories?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each product has its own Redis counter: flash:{sale_id}:inventory:{product_id}. The Lua decrement script takes the product key as a parameter. For bundle purchases (buy product A + B together): decrement both counters atomically using a single Lua script that checks both before decrementing either. Multi-key Lua scripts must have all keys on the same Redis slot — use hash tags to ensure co-location: {sale123}_productA, {sale123}_productB. This forces both keys to the same Redis cluster slot. A Lua script can then atomically check and decrement both. For multiple products in separate inventory buckets without co-location constraints: use Lua MULTI commands within the script to check all before decrementing any (Redis Lua supports conditional execution).”
}
},
{
“@type”: “Question”,
“name”: “How does the system handle payment failure after inventory has been reserved?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Payment failure after Redis inventory reservation but before order confirmation is committed: (1) Release the Redis counter: INCRBY flash:{sale_id}:inventory quantity (add back the reserved units). (2) Mark the order as PAYMENT_FAILED in the database. (3) Notify the user of the failure and offer retry with a different payment method. The user has a retry window (same session token) to attempt payment again, subject to the inventory still being available (re-run the Redis Lua check). For the async Kafka-based processing: the worker that processes the order detects a payment failure, performs a compensating transaction (increment Redis counter + set order status), and triggers the user notification. Idempotency: if the worker crashes and retries, the inventory should only be released once (use idempotency_key on the compensation transaction).”
}
}
]
}
Asked at: Shopify Interview Guide
Asked at: Airbnb Interview Guide
Asked at: Snap Interview Guide
Asked at: Twitter/X Interview Guide