DEV Community

Cover image for Preventing Double Booking at 2M Concurrent Users: Redis Seat Locks + MySQL + Kafka" published: false
Abhishek tiwari
Abhishek tiwari

Posted on

Preventing Double Booking at 2M Concurrent Users: Redis Seat Locks + MySQL + Kafka" published: false

How a BookMyShow-style ticket booking system handles seat locking, idempotent payments and async events during a blockbuster release."
tags: systemdesign, architecture, redis, backend

2 million people open the app at 7:00 PM on release day. Seat A12 can be sold exactly once.

The customer experience is simple: pick seats, pay ₹600, done in under 2 minutes. Behind that is a concurrency problem. Here is how I would design it. (This is my own design, not BookMyShow's actual internals.)

The flow

Client → CDN → API Gateway → Load Balancer → Services → Redis lock → MySQL → Kafka → Notifications

  • Edge: the CDN serves static content, and the gateway and load balancer absorb the spike.
  • Core services: Movie, Theatre, Seat Lock, Booking and Payment.
  • Data: MySQL for bookings, Redis for temporary seat holds, Kafka for async events.

Step 1: Lock seats in Redis

When a user selects seats, try to take a short-lived lock:

SET seat:{show:42}:A12 <user-token> NX PX 600000
Enter fullscreen mode Exit fullscreen mode
  • NX means set only if the key doesn't exist, so the first request wins.
  • PX 600000 gives a 10-minute hold. If the user abandons payment, the lock expires and no cleanup job is needed.

Users usually pick several seats, so the lock must be all-or-nothing. A Lua script is atomic in Redis:

-- KEYS = seat keys, ARGV[1] = user token, ARGV[2] = ttl in ms
for i = 1, #KEYS do
  if redis.call("EXISTS", KEYS[i]) == 1 then return 0 end
end
for i = 1, #KEYS do
  redis.call("SET", KEYS[i], ARGV[1], "PX", ARGV[2])
end
return 1
Enter fullscreen mode Exit fullscreen mode

In Redis Cluster, all keys in one script must be in the same hash slot. That is why the {show:42} hash tag is in the key name.

Release locks safely by checking the token first, so a user can never free someone else's lock:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
end
return 0
Enter fullscreen mode Exit fullscreen mode

Step 2: MySQL is the final judge

Redis is fast, but locks can be lost during a failover. The database must be able to refuse a double booking on its own:

CREATE TABLE booking_seats (
  id         BIGINT PRIMARY KEY AUTO_INCREMENT,
  booking_id BIGINT NOT NULL,
  show_id    BIGINT NOT NULL,
  seat_id    VARCHAR(10) NOT NULL,
  UNIQUE KEY uniq_show_seat (show_id, seat_id)
) ENGINE=InnoDB;
Enter fullscreen mode Exit fullscreen mode

Write the booking and its seats in one transaction. If two requests somehow get past Redis, the second insert fails on the unique key. Redis handles the crowd, and MySQL guarantees correctness. On cancellation, delete the seat rows so the seats can be sold again.

Step 3: Idempotent payments

Users double-tap "Pay" and networks retry. Generate one idempotency key per payment attempt and store it with a unique constraint:

CREATE TABLE payment_attempts (
  idempotency_key CHAR(36) PRIMARY KEY,
  booking_id      BIGINT NOT NULL,
  status          VARCHAR(20) NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

If the same key arrives again, return the stored result instead of charging again.

Step 4: Kafka and the outbox pattern

Emails, SMS and push notifications must never slow down a booking, so they go through Kafka. The risk is saving a booking but failing to publish the event. The fix is an outbox table written in the same transaction as the booking:

CREATE TABLE outbox_events (
  id           BIGINT PRIMARY KEY AUTO_INCREMENT,
  event_type   VARCHAR(50) NOT NULL,
  payload      JSON NOT NULL,
  published_at TIMESTAMP NULL
);
Enter fullscreen mode Exit fullscreen mode

A small relay process reads unpublished rows and sends them to Kafka. Kafka delivery is at-least-once, so consumers must also be idempotent.

Step 5: Fail gracefully

  • Retries with backoff for transient errors.
  • A circuit breaker so a failing payment or SMS provider doesn't take down the flow.
  • Fallbacks, such as queueing a notification instead of dropping it.

What breaks first

Failure What protects you
Two users tap the same seat Redis NX lock
Redis restarts and loses locks MySQL unique constraint
User double-taps Pay Idempotency key
SMS provider is down Kafka + retries
Booking saved, event lost Outbox pattern

Open questions

  • How long should a seat hold last? Too short hurts payments, too long locks inventory.
  • Is a virtual waiting room worth it for blockbuster releases?
  • Where do you see hot-key problems at peak?

I'd like to hear how you'd approach this in the comments.

Top comments (0)