Imagine booking the last seat for an IPL final. At the exact same moment, another user also clicks Book Now.
Both requests hit the backend almost simultaneously.
Load Balancer
|
-------------------------------
| |
Request A Request B
| |
Booking Service Booking ServiceA naive implementation might look like this:
public void bookSeat(Seat seat) {
if (seat.isAvailable()) {
seat.setAvailable(false);
save(seat);
}
}Looks perfectly fine... until two threads execute it at the same time.
Both threads read the seat as AVAILABLE before either one updates it.
Thread A Thread B
Read AVAILABLE
Read AVAILABLE
Book Seat
Book Seat
ā Double BookingThis is called a Race Condition.
Why synchronized Isn't Enough
A common interview answer is to use Java's synchronized keyword.
public synchronized void bookSeat() {
...
}This works only when your application is running on one JVM.
Large-scale applications rarely run on a single machine.
Load Balancer
|
-----------------------------------
| | |
Server 1 Server 2 Server 3Each server has its own JVM.
A synchronized method on Server 1 has absolutely no effect on Server 2.
This is why distributed systems cannot rely solely on Java synchronization.
Thread Pools Instead of Thousands of Threads
Imagine 20,000 users trying to book tickets within a few seconds.
Creating one thread per request would quickly exhaust memory and CPU resources.
Instead, servers use thread pools.
ExecutorService executor =
Executors.newFixedThreadPool(100);
executor.submit(() -> bookingService.bookSeat(101));Worker threads are reused, reducing thread creation overhead and improving throughput.
Database Transactions
The database becomes the source of truth.
A booking usually executes inside a transaction.
@Transactional
public void bookSeat(Long seatId) {
...
}If something fails midway, the transaction rolls back automatically.
No partially booked seats.
Pessimistic Locking
One solution is to lock the row before updating it.
SELECT *
FROM seats
WHERE seat_id = 101
FOR UPDATE;While one transaction holds the lock, every other transaction waits.
Thread A
LOCK Seat 101
Book Seat
Commit
-------------------
Thread B
Wait...
Seat already bookedOnly one booking succeeds.
Optimistic Locking
Large applications prefer optimistic locking because conflicts are relatively rare.
Each row stores a version number.
Seat
id 101
status AVAILABLE
version 5The update succeeds only if the version hasn't changed.
UPDATE seats
SET status='BOOKED',
version = version + 1
WHERE seat_id = 101
AND version = 5;If another transaction already booked the seat, zero rows are updated.
The application simply returns:
Seat already booked.
No locking required.
Distributed Locking
Suppose two different application servers receive booking requests simultaneously.
Server A Server B
Book Seat 101 Book Seat 101Many systems use Redis to acquire a distributed lock.
SET seat:101 locked NX EX 10If the command succeeds, that server owns the lock.
Every other server immediately knows someone else is processing the booking.
A Typical Booking Flow
User Clicks Book
|
Acquire Redis Lock
|
Begin Database Transaction
|
Check Seat Availability
|
Update Seat Status
|
Commit Transaction
|
Release Redis LockThis ensures that only one user can successfully reserve a seat.
Interview Takeaways
If an interviewer asks how platforms like BookMyShow prevent double booking, mentioning only synchronized isn't enough.
A strong answer should include:
- Thread pools (
ExecutorService) for efficient request processing. - Database transactions to maintain consistency.
- Optimistic or pessimistic locking for concurrent updates.
- Distributed locking using Redis or ZooKeeper across multiple servers.
- Idempotency to safely handle retries without duplicate bookings.
Modern distributed systems solve concurrency using multiple layers of protection rather than relying on a single synchronization mechanism.
The biggest lesson is this: multithreading is only one piece of the puzzle. At scale, correctness comes from combining threads, transactions, locks, and distributed coordination.