DEV Community

Jeremiah Deku
Jeremiah Deku

Posted on

How I Fixed a Race Condition in a Live Seat Booking System (And Lost Sleep Over It)

๐—ง๐˜„๐—ผ ๐˜‚๐˜€๐—ฒ๐—ฟ๐˜€. ๐—ข๐—ป๐—ฒ ๐˜€๐—ฒ๐—ฎ๐˜. ๐—ง๐—ผ๐˜๐—ฎ๐—น ๐—ฐ๐—ต๐—ฎ๐—ผ๐˜€.
It was a regular Tuesday on a client project โ€” a bus ticket booking platform. Everything looked fine until a support ticket landed: two passengers had been assigned the same seat on the same trip.

My first instinct? "That can't happen โ€” I have a check before booking." Famous last words.

๐—ช๐—ต๐—ฎ๐˜ ๐˜„๐—ฎ๐˜€ ๐—ฎ๐—ฐ๐˜๐˜‚๐—ฎ๐—น๐—น๐˜† ๐—ต๐—ฎ๐—ฝ๐—ฝ๐—ฒ๐—ป๐—ถ๐—ป๐—ด:

User A and User B both queried the seat at nearly the same millisecond. Both saw it as available. Both proceeded to book. The database updated twice โ€” same seat, two owners.

๐— ๐˜† ๐—ณ๐—ถ๐—ฟ๐˜€๐˜ (๐˜„๐—ฟ๐—ผ๐—ป๐—ด) ๐—ฎ๐˜๐˜๐—ฒ๐—บ๐—ฝ๐˜๐˜€

I tried a simple if check before updating โ€” reading the seat status, then writing. Seemed logical. Broke immediately under any real concurrency. Read-then-write is not atomic. The window between those two operations is exactly where race conditions live.

Then I tried a client-side flag. Also wrong. Socket.io events can arrive out of order, and the UI state is not a source of truth.

The fix: atomic update + real-time lock broadcast

The solution was to 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.

// Atomic Updates + Socket.io Real-Time Lock

async function reserveSeat(tripId, seatNo, userId) {

// Prevents Race Conditions
const trip = await Trip.findOneAndUpdate(
{ _id: tripId, "seats.no": seatNo, "seats.status": "available" },
{ $set: { "seats.$.status": "locked", "seats.$.heldBy": userId } },
{ new: true }
);

if (!trip) throw new Error("Seat already taken!");

// Broadcast the 'Lock' to all travelers
io.emit("seat_locked", { tripId, seatNo });
console.log(Seat ${seatNo} secured for User ${userId});
}

๐—ช๐—ต๐˜† ๐˜๐—ต๐—ถ๐˜€ ๐˜„๐—ผ๐—ฟ๐—ธ๐˜€ โ€” ๐—น๐—ถ๐—ป๐—ฒ ๐—ฏ๐˜† ๐—น๐—ถ๐—ป๐—ฒ

1๏ธโƒฃ ๐™๐™๐™š ๐™ฆ๐™ช๐™š๐™ง๐™ฎ ๐™˜๐™ค๐™ฃ๐™™๐™ž๐™ฉ๐™ž๐™ค๐™ฃ ๐™ž๐™จ ๐™ฉ๐™๐™š ๐™œ๐™ช๐™–๐™ง๐™™
"seats.status": "available" means MongoDB only updates if the seat is still available at the exact moment of the write. If two requests arrive simultaneously, only one will match โ€” the other gets null.

2๏ธโƒฃ ๐™›๐™ž๐™ฃ๐™™๐™Š๐™ฃ๐™š๐˜ผ๐™ฃ๐™™๐™๐™ฅ๐™™๐™–๐™ฉ๐™š ๐™ž๐™จ ๐™–๐™ฉ๐™ค๐™ข๐™ž๐™˜ ๐™–๐™ฉ ๐™ฉ๐™๐™š ๐™™๐™ค๐™˜๐™ช๐™ข๐™š๐™ฃ๐™ฉ ๐™ก๐™š๐™ซ๐™š๐™ก
MongoDB guarantees that the find-and-update happens as a single operation. No other write can slip in between. This is the core of the fix.

3๏ธโƒฃ ๐™๐™๐™š ๐™ฃ๐™ช๐™ก๐™ก ๐™˜๐™๐™š๐™˜๐™  ๐™ž๐™จ ๐™ฎ๐™ค๐™ช๐™ง ๐™š๐™ง๐™ง๐™ค๐™ง ๐™—๐™ค๐™ช๐™ฃ๐™™๐™–๐™ง๐™ฎ
If trip is null, the seat was already taken. Throw a clear error โ€” don't silently fail. The client can catch this and show the user a friendly message.

4๏ธโƒฃ ๐™Ž๐™ค๐™˜๐™ ๐™š๐™ฉ.๐™ž๐™ค ๐™—๐™ง๐™ค๐™–๐™™๐™˜๐™–๐™จ๐™ฉ๐™จ ๐™ฉ๐™๐™š ๐™ก๐™ค๐™˜๐™  ๐™ž๐™ข๐™ข๐™š๐™™๐™ž๐™–๐™ฉ๐™š๐™ก๐™ฎ
Once the DB confirms the lock, we emit seat_locked to all connected clients. Every user's seat map updates in real time โ€” no polling, no stale UI.

๐—ง๐—ต๐—ฒ ๐—ฏ๐—ถ๐—ด๐—ด๐—ฒ๐˜€๐˜ ๐—น๐—ฒ๐˜€๐˜€๐—ผ๐—ป
Race conditions don't announce themselves. They hide behind low traffic and happy paths. This bug only surfaced because two users happened to book at the same moment โ€” something that's guaranteed to happen at scale.

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's atomic.

๐ป๐‘Ž๐‘ฃ๐‘’ ๐‘ฆ๐‘œ๐‘ข ๐‘Ÿ๐‘ข๐‘› ๐‘–๐‘›๐‘ก๐‘œ ๐‘Ÿ๐‘Ž๐‘๐‘’ ๐‘๐‘œ๐‘›๐‘‘๐‘–๐‘ก๐‘–๐‘œ๐‘›๐‘  ๐‘–๐‘› ๐‘ฆ๐‘œ๐‘ข๐‘Ÿ ๐‘œ๐‘ค๐‘› ๐‘๐‘Ÿ๐‘œ๐‘—๐‘’๐‘๐‘ก๐‘ ? ๐ผ'๐‘‘ ๐‘™๐‘œ๐‘ฃ๐‘’ ๐‘ก๐‘œ โ„Ž๐‘’๐‘Ž๐‘Ÿ โ„Ž๐‘œ๐‘ค ๐‘ฆ๐‘œ๐‘ข โ„Ž๐‘Ž๐‘›๐‘‘๐‘™๐‘’๐‘‘ ๐‘–๐‘ก.

Top comments (2)

Collapse
 
muhammad_tahir_hasni profile image
Muhammad Tahir Hasni

Nice

Collapse
 
jeremiah_dek profile image
Jeremiah Deku