Nazmul Khalid
Engineering

Preventing Double-Booking in a Real-Time Appointment System

2026-09-21

Nazmul KhalidNazmul Khalid — Lead Software Engineer, 10+ years building production systems

Key takeaway

Preventing double-booked appointment slots by checking-then-writing in application code fails under concurrent requests. DocBee instead pushes the constraint into MongoDB itself: a unique compound index on doctor/date/slot rejects a second simultaneous hold at the storage layer, a TTL index expires abandoned holds automatically, and the booking, payment ledger entry, and hold removal all commit in one transaction — with Socket.IO broadcasting the result so the slot disappears from other patients' screens instantly.

A telemedicine marketplace lives or dies on one guarantee: when a patient taps "book," that slot is theirs. If two patients can end up holding the same doctor's slot at the same time, the product doesn't work — a doctor shows up to a double-booked consultation, or a patient pays for a slot that's already gone.

The obvious first approach is to check-then-write in application code: read the slot, confirm it's free, then create the booking. That works until two requests arrive close enough together that both read "free" before either writes. Under real traffic, "close enough together" happens more often than people expect, and a race condition that only shows up under concurrent load is one of the worst kinds of bug to chase — it won't reproduce in a manual test, and it won't show up in a demo.

Push the constraint into the database

DocBee doesn't try to prevent the race in application code at all. When a patient picks a slot, the API creates a hold document with a MongoDB unique compound index on doctor, date, and slot. If a second, simultaneous hold attempt targets that exact combination, the database rejects the write outright — it fails at the storage layer, not inside application logic that has to remember to check correctly every single time.

This is a meaningfully different guarantee than an application-level check. A unique index is enforced by the database itself, on every write, regardless of which code path created it, how many API instances are running, or whether a future engineer forgets to add the check to a new endpoint. The invariant lives in the schema, not in someone's memory.

Cleaning up abandoned holds without a cron job

Locking a slot the instant a patient starts checkout raises an obvious follow-up problem: what happens if they abandon the flow? The slot can't stay locked forever.

Rather than run a scheduled job to sweep for stale holds, the hold document carries a MongoDB TTL index. The document expires and is removed automatically once its window passes — no cleanup process to write, deploy, monitor, or forget to run. The slot frees itself.

Making the booking atomic, and making it visible

Confirming a booking touches more than one piece of state: the booking record itself, a payment ledger entry, and the removal of the hold. All three happen inside a single MongoDB transaction, so there's no window where a booking exists without its payment record, or a hold lingers after a booking is confirmed.

The last piece is making the result visible immediately to everyone else looking at that doctor's calendar. Once the transaction commits, Socket.IO broadcasts the update, so a slot disappears from other patients' screens the instant it's taken — instead of them finding out only when their own booking attempt fails against a database constraint they never saw coming.

The general lesson

Application code is a bad place to enforce an invariant that absolutely cannot be violated. Database constraints (unique indexes, TTL indexes, transactions) exist precisely because they're enforced consistently, under concurrency, regardless of which code path or how many server instances are involved. Where a business rule is truly non-negotiable — "this slot has exactly one owner" — push it as close to the data as the database will let you.

This is the writeup behind DocBee.

FAQ

How do you prevent two patients from double-booking the same appointment slot?
DocBee uses a MongoDB unique compound index on doctor, date, and slot, so a second simultaneous hold on the exact same slot is rejected by the database itself, not by an application-level check that could race.
What happens if a patient abandons checkout after locking a slot?
The hold document carries a MongoDB TTL index and expires automatically once its window passes — no scheduled cleanup job needed.
Why use a database constraint instead of an application-level check to prevent race conditions?
A unique index is enforced by the database on every write, regardless of code path, server instance count, or whether a future engineer forgets to add the check — a guarantee application code alone can't make under concurrent load.

Related articles