Core Challenge and Entities
A flash sale creates a sudden traffic spike (100x-1000x normal load) competing for limited inventory. The core problems: (1) oversell prevention (more orders than items), (2) fairness (all users get equal access), (3) availability (system stays up under load). FlashSale: sale_id, product_id, total_quantity, sold_quantity, price, start_time, end_time, status (UPCOMING, ACTIVE, SOLD_OUT, ENDED). SaleInventory: sale_id, quantity_remaining (Redis counter, authoritative during sale). SaleOrder: order_id, sale_id, user_id, status (RESERVED, CONFIRMED, EXPIRED, CANCELLED), reserved_at, expires_at (reserved_at + 10 minutes), confirmed_at. QueueEntry: entry_id, sale_id, user_id, position, entered_at, token (signed JWT for purchase authorization).
Pre-Sale Queue and Fair Access
class FlashSaleQueueService:
def join_queue(self, sale_id: int, user_id: int) -> dict:
sale = self.db.get_sale(sale_id)
if sale.status != "UPCOMING" and sale.status != "ACTIVE":
raise SaleNotAvailable()
# Deduplicate: one queue entry per user per sale
key = f"queue:{sale_id}:user:{user_id}"
if self.redis.exists(key):
position = self.redis.zscore(f"queue:{sale_id}", user_id)
return {"position": int(position), "already_queued": True}
# Add to sorted set (score = timestamp for FIFO ordering)
position = self.redis.zadd(
f"queue:{sale_id}", {user_id: time.time()}
)
self.redis.setex(key, 86400, "1") # dedup key TTL 24h
queue_size = self.redis.zcard(f"queue:{sale_id}")
return {"position": queue_size, "estimated_wait_seconds": queue_size * 2}
def release_batch(self, sale_id: int, batch_size: int):
# Called every 2 seconds: release next batch from queue
# batch_size = min(remaining_inventory, users_to_let_in)
entries = self.redis.zpopmin(f"queue:{sale_id}", batch_size)
for user_id, _ in entries:
token = self._generate_purchase_token(sale_id, user_id, ttl=300)
self._notify_user(user_id, f"Your turn! Complete purchase within 5 minutes.")
self.redis.setex(f"token:{sale_id}:{user_id}", 300, token)
Atomic Inventory Reservation with Redis
class InventoryReservationService:
def reserve(self, sale_id: int, user_id: int,
purchase_token: str) -> str:
# Validate purchase token (signed JWT with sale_id, user_id, expiry)
if not self._verify_token(purchase_token, sale_id, user_id):
raise InvalidToken()
# Atomic decrement: prevents oversell
remaining = self.redis.decr(f"inventory:{sale_id}")
if remaining str:
expires_at = datetime.utcnow() + timedelta(minutes=10)
order = self.db.insert("sale_orders", {
"sale_id": sale_id,
"user_id": user_id,
"status": "RESERVED",
"reserved_at": datetime.utcnow(),
"expires_at": expires_at
})
return order.order_id
Expiry and Inventory Recovery
Reservations expire after 10 minutes if unpaid. Background job (runs every 30 seconds): SELECT order_id, sale_id FROM sale_orders WHERE status=’RESERVED’ AND expires_at < NOW(). For each expired order: UPDATE status='EXPIRED', then INCR inventory:{sale_id} in Redis (return item to pool), then notify the next queued user. This recycles inventory from abandoned reservations back into the pool. Redis key expiry callbacks (keyspace notifications) can also trigger recovery, but a polling job is simpler and more reliable. Race condition: if the user pays at the exact moment of expiry, use an atomic compare-and-swap: only expire if status is still RESERVED (not CONFIRMED).
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you prevent overselling in a flash sale system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use Redis DECR (atomic decrement) as the authoritative inventory counter. Before the sale, SET inventory:{sale_id} to total_quantity in Redis. On each purchase attempt, DECR the counter. If the result is negative (< 0), immediately INCR to undo and return SoldOut error. Redis is single-threaded: DECR is atomic with no race condition. The database order record is created asynchronously after the Redis reservation succeeds. This is faster than using DB transactions for inventory checks, which would bottleneck at the database under flash sale traffic.”}},{“@type”:”Question”,”name”:”How do you implement a fair queue for a flash sale?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a Redis sorted set: ZADD queue:{sale_id} {timestamp} {user_id}. The score (Unix timestamp of joining) ensures FIFO ordering. Deduplicate with a separate Redis key per user to prevent multiple queue entries. Periodically (every 2 seconds), ZPOPMIN releases a batch of users from the queue – the batch size matches the pace at which reservations can be processed. Each released user receives a signed purchase token (JWT with sale_id, user_id, expiry) valid for 5 minutes. Only token holders can attempt to reserve inventory.”}},{“@type”:”Question”,”name”:”How do you recover inventory from abandoned reservations in a flash sale?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Set a reservation expiry (10 minutes) on each SaleOrder. A background job (runs every 30 seconds) queries for RESERVED orders with expires_at < NOW(). For each expired order: atomically update status to EXPIRED (WHERE status='RESERVED' to prevent race with payment completion), then INCR inventory:{sale_id} in Redis to return the item to the pool. Then notify the next user in the queue. Use compare-and-swap on the status update: if a user pays at the exact expiry moment, the payment confirmation sets status=CONFIRMED first, and the expiry job finds status != RESERVED and skips it.”}},{“@type”:”Question”,”name”:”How do you protect the flash sale system from traffic spikes?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Five layers: (1) Queue: do not let all users hit the purchase endpoint simultaneously – put them in a queue and release in controlled batches. (2) Rate limiting: max 1 queue-join request per user per sale. (3) CDN: serve the pre-sale countdown page from CDN edge (no origin hit). (4) Read replicas: product detail page served from DB read replica. (5) Circuit breaker: if the payment processor is slow, queue payment jobs instead of blocking HTTP requests. The Redis queue absorbs the spike; the database and payment processor only see metered traffic matching inventory throughput.”}},{“@type”:”Question”,”name”:”How does the flash sale reservation expiry interact with the inventory counter?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The Redis counter represents available-to-reserve inventory. Reservation decrements it immediately. Expiry increments it back. Confirmed payment does not change the counter (the reservation already decremented it). This means: total_quantity = redis_counter + active_reservations + confirmed_orders. If the Redis counter is 0 and 10 reservations are pending, the "effective" available inventory is 0 until some expire. The 10-minute expiry window balances user experience (enough time to pay) against holding inventory hostage on abandoned reservations.”}}]}
Shopify system design interviews cover flash sales and inventory. See design patterns for Shopify interview: flash sale and inventory system design.
Airbnb system design rounds cover reservation and availability systems. See patterns for Airbnb interview: booking and reservation system design.
Stripe system design interviews cover atomic payment reservations. Review patterns for Stripe interview: payment reservation and race condition prevention.