How I Fixed a Race Condition in a Live Seat Booking System (And Lost Sleep Over It)
Source: Dev.to
The Problem
Two users, one seat – total chaos.
Both User A and User B queried the seat at nearly the same millisecond, saw it as available, and proceeded to book. The database was updated twice, resulting in the same seat having two owners.
Failed Approaches
Simple read‑then‑write check
I first tried a straightforward if check: read the seat status, then write the new booking. This broke immediately under any real concurrency because the read‑then‑write sequence is not atomic. The tiny window between the two operations is exactly where race conditions thrive.
Client‑side flag
Next I added a client‑side flag to indicate a lock. This also failed—Socket.io events can arrive out of order, and the UI state is not a reliable source of truth.
The Solution
Collapse the read and the write into a single atomic MongoDB operation and pair it with a Socket.io broadcast so every connected client sees the lock instantly.
// reserveSeat.js
async function reserveSeat(tripId, seatNo, userId) {
// Atomically find the seat if it is still free and mark it as taken
const seat = await Seats.findOneAndUpdate(
{ tripId, seatNo, taken: false }, // query condition (the guard)
{ $set: { taken: true, userId } }, // update (atomic at document level)
{ returnDocument: 'after' } // return the updated document
);
if (!seat) {
throw new Error("Seat already taken!");
}
// Broadcast the lock to all travelers
io.emit('seatLocked', {
seatNo,
userId,
message: `Seat ${seatNo} secured for User ${userId}`
});
}Key Steps
- The query condition is the guard – only match seats that are still free.
findOneAndUpdateis atomic at the document level – the read and write happen in a single operation.- The null check is your error boundary – if no document was returned, the seat was already taken.
- Socket.io broadcasts the lock immediately – all clients receive the update in real time.
The Biggest Lesson
The fix isn’t just about MongoDB; it’s a mindset shift: never trust a read before a write in a concurrent system. Push your guard logic into the database operation itself, where it can be atomic.
Your Turn
Have you run into race conditions in your own projects? I’d love to hear how you handled them.