Example: Flash sale for 1,000 iPhones with 1,000,000 users clicking βBuyβ at the same time.
π§ Why Flash Sales Are Hard
Flash sales look simple:
βWe have 1,000 items. When inventory reaches zero, stop selling.β
But in production, flash sales are one of the hardest problems in distributed systems.
- Millions of concurrent requests
- Multiple app servers
- Eventually consistent caches
- Databases that cannot absorb spikes
- Redis that can still melt under pressure
The real challenge is not inventory.
It is correctness under extreme contention.
π― Core Requirements
- β No overselling (ever)
- β‘ Low latency
- π§± Handle massive traffic spikes
- π₯ Protect Redis & database
- π‘οΈ Graceful failure & recovery
π§© Core Insight
Flash sales are load-shedding problems disguised as inventory problems.
If only 1,000 users can succeed, then 999,000 users must fail fast β cheaply and safely.
1οΈβ£ Naive Database Approach (Incorrect)
SELECT stock FROM inventory WHERE product_id = 1;
IF stock > 0:
UPDATE inventory SET stock = stock - 1;
CREATE order;
β What goes wrong
- Two requests read stock = 1
- Both decrement
- Inventory oversold
Verdict: β Incorrect even at low scale.
2οΈβ£ Database Locking (Correct but Not Scalable)
SELECT stock
FROM inventory
WHERE product_id = 1
FOR UPDATE;
β Pros
- Strong consistency
- No overselling
β Cons
- Requests serialized
- Database becomes bottleneck
- Throughput collapses
Use only for very low traffic.
3οΈβ£ Atomic SQL Update (Better, Still Limited)
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 1 AND stock > 0;
β Pros
- Simple
- Correct
β Cons
- Database still hot
- Does not scale for flash sales
4οΈβ£ Redis as Inventory Gatekeeper
DECR inventory:iphone
If result < 0 β rollback and reject.
Redis + Lua (Atomic)
local stock = redis.call("GET", KEYS[1])
if tonumber(stock) > 0 then
redis.call("DECR", KEYS[1])
return 1
else
return 0
end
β Pros
- Very fast
- No overselling
π¨ Hidden Problem: Redis Can Still Go Down
Scenario:
- Inventory: 1,000
- Users: 1,000,000
- All users hit the same Redis key
Even Redis has limits:
- Single hot shard
- Network saturation
- CPU contention
- Timeouts and failures
Correctness without traffic control is failure.
5οΈβ£ Mandatory Defense: Early Rejection
Local In-Memory Gate
Each app instance keeps a small local counter.
if localRemaining == 0:
reject immediately
β Pros
- Protects Redis massively
- Very cheap
β Cons
- Soft limits
- Needs reconciliation
6οΈβ£ Batch Inventory Allocation (High Impact)
DECRBY inventory:iphone 20
Serve 20 users locally before hitting Redis again.
β Pros
- Redis calls β inventory count
- Huge throughput improvement
β Cons
- Over-allocation needs give-back logic
- Slight fairness skew
7οΈβ£ Redis Hot Shard Problem & Striping
Single key β single shard β overload.
Solution: Bucketed inventory.
inv:iphone:0
inv:iphone:1
...
inv:iphone:49
β Pros
- Load distributed across shards
β Cons
- Retry logic near tail
8οΈβ£ Token / Permit Model
1 token = 1 purchase. Pre-generate 1,000 tokens.
β Pros
- Impossible to oversell
- Clean mental model
β Cons
- Token cleanup on failure
9οΈβ£ Admission Control
Only allow slightly more users than inventory to proceed.
INCR sale:attempts
reject if > 1500
Effect
- Redis protected
- Fast rejection
π Queue-Based Flash Sale (Extreme Scale)
- Users enqueue buy requests
- Workers process sequentially
- Stop after inventory exhausted
Trade-off
- Higher latency
- Excellent stability
π Failure Handling
- Payment failure: TTL + release inventory
- Duplicate clicks: Idempotency keys
- Redis crash: Reload from DB
- Cache drift: Reconciliation job
βοΈ Trade-off Summary
| Approach | Scale | Redis Load | Complexity |
|---|---|---|---|
| DB Lock | Low | None | Low |
| Redis DECR | High | High | Medium |
| Batch Allocation | Very High | Low | High |
| Queue-Based | Extreme | Minimal | High |
π Final Thought
A successful flash sale is not about selling fast.
It is about rejecting users correctly while protecting shared state.
If you enjoyed this, follow for more system design deep dives.
Top comments (0)