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
-
NXmeans set only if the key doesn't exist, so the first request wins. -
PX 600000gives 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
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
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;
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
);
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
);
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)