The Core Challenge
A flash sale sells a limited quantity (e.g., 1000 units) at a discounted price for a short window (15-60 minutes). The challenge: at sale start, thousands of concurrent buyers attempt to purchase simultaneously. The system must: sell exactly the available quantity (no oversell, no undersell), handle the burst without data corruption, and provide a fair user experience.
Core Entities
FlashSale: sale_id, product_id, quantity_available, quantity_sold, price_cents, start_time, end_time, status (SCHEDULED, ACTIVE, ENDED, SOLD_OUT). SaleOrder: order_id, sale_id, user_id, quantity, status (RESERVED, PAID, CANCELLED, EXPIRED), reserved_at, expires_at. UserSaleEligibility: (user_id, sale_id) — one purchase per user per sale.
Inventory Reservation with Redis
At sale start, load available quantity into Redis: SET sale:{id}:stock 1000. For each purchase attempt: DECRBY sale:{id}:stock 1. If the result >= 0, the reservation succeeded. If the result is negative, INCRBY back (compensate) and reject with “Sold Out.” This Redis DECRBY is atomic — no race conditions. At very high concurrency (100K requests/second), Redis handles this easily (single-threaded, 1M+ ops/second).
Dual write: after Redis DECRBY succeeds, write a RESERVED SaleOrder to the database. Use a checkout TTL (10 minutes): if the user does not complete payment, a background job cancels the order and INCRBY the Redis stock counter. This returns the slot to the pool for other buyers.
Queue-based Checkout for Fairness
When demand far exceeds supply (10K buyers, 1K units), queue buyers at sale start instead of letting all hit Redis simultaneously. At T-0: accept buyers into a virtual queue (Redis LIST: RPUSH sale:{id}:queue {user_id}). A worker pops from the queue (LPOP) and processes purchases in order. Buyers are shown their queue position. Top N buyers (N = available quantity) get purchase slots; the rest are notified “sold out.” This is fairer than pure first-come-first-served race conditions where network proximity to the server determines success.
Preventing Oversell
Three layers of protection:
Layer 1 (Redis): atomic DECRBY gate — rejects requests when stock reaches 0. Fast, in-memory, first line of defense.
Layer 2 (Database): after inserting the RESERVED order, verify: UPDATE flash_sales SET quantity_sold = quantity_sold + 1 WHERE sale_id = X AND quantity_sold < quantity_available. If 0 rows updated, the database count confirms no stock — rollback the order and restore Redis counter.
Layer 3 (Audit): background reconciliation job runs every 5 minutes during the sale. Counts RESERVED+PAID orders and compares to quantity_sold. Alerts if discrepancy exceeds 0 (should never happen if layers 1 and 2 are correct).
Per-User Purchase Limits
One purchase per user per sale. Check: INSERT INTO user_sale_eligibility (user_id, sale_id) ON CONFLICT DO NOTHING RETURNING *. If no row returned, user already purchased — reject. The unique constraint (user_id, sale_id) enforces this atomically across concurrent requests. For quantity limits (max 2 per user): store quantity in user_sale_eligibility and use a SELECT FOR UPDATE check before allowing additional units.
Pre-sale Warm-up
Before the sale starts: pre-populate Redis cache with product details, stock counter, and user eligibility blacklists (accounts flagged for abuse). Pre-warm CDN for product page assets. Send countdown emails 1 hour and 5 minutes before sale. At T-5 minutes: enable the waitlist page (to capture user intent without hitting the DB). Scale up application servers and Redis cluster to handle the burst. Alert the on-call team if auto-scaling takes more than 2 minutes before the sale starts.
Post-sale Processing
After all units are reserved: transition sale status to SOLD_OUT. Process payments asynchronously: for each RESERVED order, charge the user via the payment gateway. On payment success: PAID. On failure (card declined, timeout): CANCELLED, restore Redis stock, offer to the next buyer in queue (if any). After the TTL window (10 minutes): cancel all remaining RESERVED orders (users who did not complete checkout).
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent overselling in a flash sale with high concurrency?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use Redis atomic DECRBY as the first gate. At sale start: SET sale:{id}:stock 1000. On each purchase: result = DECRBY sale:{id}:stock 1. If result >= 0, reservation is allowed. If result < 0: INCRBY to restore (compensate) and reject with sold out. Redis DECRBY is single-threaded and atomic — no two requests can decrement the same counter simultaneously. This handles 100K+ concurrent requests without race conditions. Second gate at the database: UPDATE flash_sales SET quantity_sold = quantity_sold + 1 WHERE sale_id = X AND quantity_sold < quantity_available returns 0 rows if stock is exhausted at the DB level — final safety net if Redis and DB get out of sync."
}
},
{
"@type": "Question",
"name": "How do you implement a virtual queue for flash sale fairness?",
"acceptedAnswer": {
"@type": "Answer",
"text": "At sale start (T+0), instead of all users hitting the inventory directly, put them in a queue: RPUSH sale:{id}:queue {user_id} in Redis. Each enqueue returns the queue position. A pool of workers pops from the queue (LPOP) and processes purchases in FIFO order. Users with queue positions 1-1000 (for 1000 units) get purchase slots. Users beyond position 1000 receive sold out notification immediately. Display real-time queue position updates via WebSocket. The queue absorbs the burst: Redis handles 1M RPUSH/second, the workers process at their sustainable rate. This is fairer than a pure race where network proximity to the server determines who wins."
}
},
{
"@type": "Question",
"name": "How do you handle checkout timeouts in a flash sale?",
"acceptedAnswer": {
"@type": "Answer",
"text": "When a user reserves inventory (DECRBY succeeds), create a RESERVED order with expires_at = NOW() + 10 minutes. The user has 10 minutes to complete payment. A background job runs every minute: SELECT * FROM sale_orders WHERE status = RESERVED AND expires_at < NOW(). For each expired order: cancel it (status=CANCELLED), INCRBY sale:{id}:stock 1 (restore Redis counter), offer the slot to the next user in the queue (if any). This recirculates abandoned slots back to waiting buyers. The 10-minute window must be short enough to not block other buyers long, but long enough for a normal checkout flow (payment form, credit card entry)."
}
},
{
"@type": "Question",
"name": "How do you enforce one purchase per user in a flash sale?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Insert a record into a user_sale_eligibility table with a unique constraint on (user_id, sale_id): INSERT INTO user_sale_eligibility (user_id, sale_id, purchased_at) ON CONFLICT (user_id, sale_id) DO NOTHING RETURNING *. If no rows are returned, the user already has a record — reject the purchase attempt. Do this check BEFORE the Redis DECRBY to avoid decrementing stock for ineligible users. For multiple quantity limits (max 2 per user): store quantity_purchased in the row and use SELECT FOR UPDATE to atomically check and increment. At very high scale, pre-load known-ineligible users into a Redis SET (SISMEMBER) for O(1) pre-check before hitting the database."
}
},
{
"@type": "Question",
"name": "How do you pre-warm infrastructure before a flash sale starts?",
"acceptedAnswer": {
"@type": "Answer",
"text": "T-60 minutes: pre-populate Redis with stock counter (SET sale:{id}:stock 1000), product metadata cache, and user eligibility blacklists (accounts flagged for abuse or fake accounts). Configure auto-scaling policies to pre-scale application servers to 5x normal capacity — do not rely on auto-scaling reacting to the burst (it is too slow). T-30 minutes: run a load test to verify the scaled infrastructure can handle expected peak. T-5 minutes: enable the waitlist page (captures user intent, avoids pre-sale DB hits). Send push notifications and emails to remind registered users. T-0: flip the sale status to ACTIVE in Redis. Monitor error rate, queue depth, and Redis memory every 30 seconds during the sale. Have rollback procedures ready if any component degrades."
}
}
]
}
Asked at: Shopify Interview Guide
Asked at: Stripe Interview Guide
Asked at: DoorDash Interview Guide
Asked at: Snap Interview Guide