SkillPilotAI

Concurrency Control: Inventory, Flash Sale & Overselling

1000 phones and 10 lakh buyers: not a single extra phone should be sold, and the system should not crash either.

38.1 Why is it used?

  • If two users buy the last item together, both see stock 1 and both orders go through; this is called overselling.
  • In a flash sale thousands of writes per second hit a single product row, and DB row lock contention slows everything down.
  • Overselling means cancelled orders, refunds, customer complaints and brand damage.
  • In bookings like IRCTC Tatkal, lakhs of people click together for limited seats, and fairness is needed too.
  • Race conditions do not show up in testing, only under real traffic, so they must be solved at the design level.

38.2 How it works

  • Pessimistic locking: SELECT stock FROM items WHERE id=1 FOR UPDATE; lock the row, then check and update. Safe but slow under high contention.
  • Optimistic locking: a version column; UPDATE items SET stock=stock-1, version=version+1 WHERE id=1 AND version=5; if 0 rows are updated, retry.
  • Atomic conditional update: UPDATE items SET stock=stock-1 WHERE id=1 AND stock>0; check + decrement in a single statement.
  • Redis atomic DECR / Lua: keep stock in Redis, check stock>0 then DECR in a Lua script; Redis is single-threaded so it is atomic.
  • Queue-based ordering: put all buy requests in Kafka/SQS and let a single consumer per product process them sequentially, giving FIFO fairness.
  • Reservation with TTL: hold the item in the cart for 10 minutes; if payment does not happen, release the stock back.
  • Stock sharding: split 1000 stock into 10 buckets of 100 each so the whole load does not land on one hot key.
  • Async DB sync: fast decrement in Redis, then final order and stock update in the DB through a queue (eventual consistency).
Wrong: read-then-write race
  1. 1.User A: SELECT stock -> 1
  2. 2.User B: SELECT stock -> 1
  3. 3.User A: UPDATE stock=0, order A created
  4. 4.User B: UPDATE stock=-1, order B created too

Overselling: 1 phone, 2 orders; cancellations, refunds, complaints. Doing GET and then DECR in Redis is the same mistake.

Right: atomic conditional update
  1. 1.UPDATE items SET stock = stock-1 WHERE id=1 AND stock > 0 (check + decrement together)
  2. 2.User A: 1 row updated, OK
  3. 3.User B: 0 rows updated, Sold Out

Options for low traffic or low conflict: Pessimistic (SELECT ... FOR UPDATE, safe but slow) and Optimistic (WHERE version=5, retry on 0 rows).

Flipkart Big Billion Day: 1000 phones, 10 lakh buyers
  1. 1.10 lakh users press Buy Now at 12:00
  2. 2.Waiting Room (Queue-it / Cloudflare) lets them in at a controlled rate
  3. 3.Redis Lua atomic: stock:phone123 = 1000; if stock>0 then DECR + token, else Sold Out (9.99 lakh users get an instant answer, DB stays safe)
  4. 4.Only the 1000 orders holding a token go into the Kafka orders topic (sequential)
  5. 5.Order Service: payment + confirm; MySQL final guard: UPDATE stock=stock-1 WHERE id=1 AND stock>0
  6. 6.Reservation TTL 10 min: on payment failure / timeout, Redis INCR returns stock to the pool

Result: 0 overselling, only ~1000 writes on the DB.

38.3 Tools you can use

ToolTypeHow it is used
Redis (DECR, Lua scripts)In-memory storeFlash sale stock counter; check-and-decrement is atomic in a Lua script, lakhs of ops per second.
MySQL / PostgreSQL row locksRelational DBStrong correctness for normal traffic with SELECT FOR UPDATE or a conditional UPDATE.
Apache Kafka / AWS SQS FIFOMessage queueSerialize orders in a queue for ordered processing and protect the DB from spikes.
DynamoDB conditional writesNoSQL DBUpdateItem with ConditionExpression stock > 0, atomic and serverless scale.
Redisson / ZooKeeper locksDistributed lockA per-product lock when multi-step logic must be atomic, but a bottleneck under high traffic.
Virtual waiting room (Queue-it, Cloudflare Waiting Room)Traffic gateHold users in a queue on the sale page and let them in at a controlled rate.

38.4 Comparison: Pessimistic Lock vs Optimistic Lock vs Redis Atomic vs Queue-based

PointPessimistic (FOR UPDATE)Optimistic (version)Redis DECR / LuaQueue-based
ThroughputLow, lock waitMedium, retries on conflictVery high, lakhs/secHigh, async
CorrectnessStrongStrongStrong in Redis, DB sync is separateStrong, sequential
High contentionLong lock queueRetry stormHandles it bestGood, absorbs the spike
User responseImmediateImmediate or retryImmediateDelayed, pending status
ComplexityLowLow-MediumMedium, Redis-DB syncHigh, async flow
Best forLow traffic, bank transferNormal e-commerce, low conflictFlash sale counterTicket booking, fairness

38.5 When to choose which

  • Pessimistic lock: when traffic is low and conflict is likely, e.g. account balance update.
  • Optimistic lock: normal product pages where conflict is rare.
  • Redis atomic DECR/Lua: flash sale where you need an instant yes/no under very high concurrency.
  • Queue-based: when fairness (first come first served) and DB protection matter, e.g. Tatkal, concert tickets.
  • Real systems use a combo: waiting room + Redis gate + queue + DB conditional update as the final guard.

38.6 Real-world example

Real world: Flipkart Big Billion Day phone flash sale (a hypothetical scenario)

  1. At 12 o clock, 1000 phones are on a Rs 1 deal and 10 lakh users press Buy Now together.
  2. First, a virtual waiting room lets users onto the sale page at a controlled rate.
  3. Stock lives in Redis as stock:phone123 = 1000. Lua script: if stock>0 then DECR and return the user a reservation token, else Sold Out.
  4. Only 1000 users get a token; the other 9.99 lakh get an instant Sold Out and their requests never reach the DB.
  5. Orders with tokens go into Kafka; the order service does the final confirmation in the DB with UPDATE stock=stock-1 WHERE stock>0.
  6. Each reservation lasts 10 minutes; on payment failure or timeout, stock is returned with INCR in Redis.
  7. Result: 0 overselling, only ~1000 writes on the DB, the site stays stable.

38.7 Interview tips

  • Doing read-then-write in separate statements (SELECT stock then UPDATE) without a lock is the classic race condition.
  • Doing GET then DECR as two separate Redis commands; use atomic Lua or check the DECR result.
  • Sending all flash sale traffic straight to the DB exhausts the DB connection pool.
  • Interview tip: explain the optimistic vs pessimistic trade-off and the Redis + queue + DB final guard architecture.