SD System Design In Depth
0%
lesson system-designinventorytransactions

Flash sale inventory locking

Module 4.2: Asynchronous Workflows & Partitioned Logs Updated Sep 17, 2026
📄 Cheat Sheet
System Design Interview: Design Ticketmaster w/ a Ex-Meta Staff Engineer
Hello Interview System Design Interview: Design Ticketmaster w/ a Ex-Meta Staff Engineer Watch on YouTube ↗

Hello Interview's Ticketmaster design is the best-explained treatment of reserving scarce inventory under a surge: reservation holds with TTLs, avoiding row-lock contention, virtual waiting rooms and consistency. The current rate-limiter video is off-topic.

0:00 / --:--

The Flash Sale Concurrency Challenge

During a mega flash sale (e.g. 1,000 PlayStation 5 consoles offered to 500,000 concurrent shoppers), hundreds of thousands of requests hit the inventory service in the exact same second. Traditional relational database transactions cause catastrophic failures:

-- THE FLASH SALE KILLER: ROW LOCK CONVOY
BEGIN TRANSACTION;
SELECT stock FROM items WHERE id = 123 FOR UPDATE;
IF stock > 0 THEN
    UPDATE items SET stock = stock - 1 WHERE id = 123;
END IF;
COMMIT;
      

When 50,000 concurrent database connections attempt to acquire a row lock on item 123, the database connection pool exhausts in milliseconds, lock waits pile up until queries hit lock-wait timeouts, and every other query that needs a connection stalls behind the hot row.

The Multi-Layer In-Memory Locking Architecture

flowchart TD Traffic["500,000 Concurrent Buyers"] --> RateLimit["1. Nginx Token Bucket Rate Limiter (Drop 80%)"] RateLimit --> App["2. Flash Sale Application Service"] App -->|"3. Atomic Lua Script: EVALSHA"| Redis[("Redis Cluster (Single-Threaded RAM)")] Redis --> StockCheck{"Stock > 0?"} StockCheck -->|"No"| SoldOut["Return 'Sold Out' (fast path, a few ms)"] StockCheck -->|"Yes (Atomic DECR)"| Reserve["Generate Reservation Token: 'res_88192'"] Reserve --> OrderQueue[("Kafka: order_reservations")] OrderQueue --> DBWorker["Asynchronous DB Worker (Batched INSERT)"] DBWorker --> Postgres[("PostgreSQL Orders DB")]

Under the Hood: Atomic Redis Lua Script

Redis executes Lua scripts atomically in its single-threaded event loop. No other command can execute between reading the stock and decrementing it, eliminating race conditions with zero database locks:

-- Keys: 1 = item_stock_key, 2 = user_reservations_set
-- Args: 1 = user_id
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then
    return -1 -- Sold out
end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
    return -2 -- Already reserved by this user
end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1 -- Success: stock reserved
      

Delayed Release Timer: If the user does not complete payment within 15 minutes, a delayed scheduler (Redis ZSET) automatically re-increments the Redis stock and invalidates the reservation token.

Worked example: funnel sizing

1,000 units, 500,000 users arriving within 10 seconds.

  • Edge or waiting room: admit users at a controlled rate, for example 5,000 per second, and send the rest to a queue page with a position number. This protects everything behind it.
  • Reservation step: an atomic Redis Lua script checks per-user limits and decrements stock. At 5,000 attempts per second, all 1,000 units are reserved within about 0.2 seconds of opening. After that the script just returns SOLD_OUT, at tens of thousands of operations per second on one shard.
  • Order persistence: 1,000 reservations are written to Kafka and then batch-inserted into Postgres. That is trivial load, where 500,000 concurrent SELECT ... FOR UPDATE on one row would have been fatal.
  • Payment: 10-minute holds. If 15 percent abandon, 150 units return to stock at expiry and are re-offered to the waiting room.

The atomic reservation (Redis Lua)

-- KEYS[1]=stock:{sku1}  KEYS[2]=buyers:{sku1}   ARGV[1]=user_id
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -2 end
local s = tonumber(redis.call('GET', KEYS[1]) or '0')
if s <= 0 then return -1 end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return s - 1

Inventory strategies compared

StrategyCorrectnessThroughput on one SKUComplexity
SELECT FOR UPDATE then UPDATEStrongHundreds per second (serialised on one row)Low
Single atomic UPDATE with WHERE stock > 0StrongA few thousand per second (still one hot row)Low
Stock split into N bucket rowsStrongN times higherMedium: pick buckets, rebalance near zero
Redis Lua counter plus async DBStrong at Redis, with a failover riskTens of thousands per second or moreMedium: reconciliation
Pre-generated tokens in a queueStrong: each token is claimed onceHighMedium
Waiting room in front of any of the aboveUnchangedProtects backendsAdds a UX component

How real systems do it

  • Alibaba's Singles' Day uses layered throttling, cached stock checks, and database hot-row optimisations (queued updates on a hot row in AliSQL).
  • Ticketmaster's Smart Queue and Cloudflare's Waiting Room meter admission so that the inventory system only sees a manageable rate.
  • Shopify throttles checkout per shop and load-tests flash-sale merchants ahead of drops.
Check yourself Question 1 of 3
Why the Lua script works recall
From: Under the Hood: Atomic Redis Lua Script

Why does running check-stock-then-decrement as a Redis Lua script prevent overselling, when the same two steps in application code do not?

Key Takeaways

Relational database row-level locking (FOR UPDATE) collapses under flash sales due to thread lock convoys.
In-memory Redis atomic Lua scripts decrement inventory in sub-milliseconds without database contention, but Redis replication is asynchronous, so a failover can lose decrements and oversell unless counts are reconciled against the database.
Successful reservations are queued asynchronously to Kafka for batched database persistence with delayed release timers.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue