Everything was working fine until two users try to book same seat at the same time

Everything was working fine until two users try to book same seat at the same time.
I was working on backend of my event ticket booking system project. It’s a simple system where users can register, search, and book tickets for events.
Everything was working fine. The logic was simple. Workflow was straightforward, until two users try to book same seat at the same time.
And that’s exactly where things went wrong.
The problem was simple.
Assume there are two users, User A and User B. Both try to book same seat at the same time. They select the seat and click the confirm booking button.
Both requests go through.
Same seat of same event booked twice.
But why did this happen ?
I had written my server code in Node.js. So when multiple requests hit a Node.js server, they are handled using the event loop.
If a request performs an asynchronous operation like a network call, database query or file reading etc.
Node.js does not wait for the result. And continues handling the other incoming requests.
This behavior of Node.js allows multiple requests to overlap in execution.
For example, when two requests hit the server at the same time let’s say Request A and Request B.
In this case Node.js picks one request say Request A and start executing it.
While executing the request it reaches the part where it checks the seats availability, which is database operation (async).
Since it’s async operation & non-blocking in nature, Node.js doesn’t wait for the result and moves to the next request that is request B in our example.
Now request B also run and hits the same database operation. So both requests A and B ask the database for available seats almost at the same time.
If there is only 1 seat available, both will see 1 seat is available. And according to our seat booking logic both requests will proceed with booking.
And that’s where Race Condition comes into picture.
At this point, the problem was clear. But the logic I had written also looked perfectly fine.
My initial approach was simple.
First, Check if requested number of seats are less than or equal to available seats. If yes, proceed with the booking.
This logic looks correct and work fine when requests come one by one.
But here the problem is, checking availability and booking are two separate steps. There is no guarantee that the data remains the same between these two step when multiple requests hit at the same time.
so yeah, it was clear that my approach was wrong. I need a better way to handle this.
So how did I solve this ?
To fix this issue, I changed my approach.
Till now, we know that my current logic works. But it’s treating seat availability check and booking as two separate steps.
And to handle this concurrent request issue, we need something which makes these steps a single unit or we can say makes them atomic.
To achieve this, I used Database Transactions.
A database transaction ensures that multiple database operations are treated as a single unit.
So instead of checking availability and booking separately, both steps now performed together.
Either both succeed, or none of them do.
You can think of it like this, either everything will happen, or nothing will happen.
For example, when you send money using a UPI app, the amount will be deducted from your account and added to the receiver’s account.
If any step fails in between, the whole transaction will be rolled back.
The same idea applies in my case, where both steps now will happen together as a single unit.
So now the question is, How did I implement this in my code ?
So I already had the logic.
I didn’t change much in the logic, I just wrapped it inside Database Transaction. This makes seat availability check and booking steps atomic.
Now, when a User clicks on booking button, a request is sent and no other request can interfere until this request completes the entire booking operations.
And one important thing.
Under the hood, the database also locks the row that is being updated, so that no other request can update it at the same time.
So yeah, Database Transaction + Row locking solves the problem for now.
But it’s not the perfect solution for this kind of problems.
Now, you might be thinking, why ?
So, the issue with this solution is that, we are locking the row during the booking process, and other requests have to wait until the current request is finished.
At the small scale, this doesn’t feel like a big problem.
But as the system grows and the number of users increases, this waiting time will start affecting performance.
Now, you might also be thinking, if this can slow down the system, why even use it ? Why not just leave it as it is ?
So this is where probability comes into the picture.
In my case, the chances of concurrent requests are low because the application is small and had only hundreds of users.
But in real world systems, we can’t ignore the fact that as the number of users increases, the chances of multiple requests hitting at the same time also increases.
And that’s when problems like this start to appear more frequently.
So even though this approach might affect the performance of the system, but it ensures correctness. And that trade off becomes important as the system scales.
This is where things started to feel less like coding and more like system design thinking.
