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 (0)