๐ง๐๐ผ ๐๐๐ฒ๐ฟ๐. ๐ข๐ป๐ฒ ๐๐ฒ๐ฎ๐. ๐ง๐ผ๐๐ฎ๐น ๐ฐ๐ต๐ฎ๐ผ๐.
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)
Nice
Thanks @muhammad_tahir_hasni