The Double Booking Problem in C# — How to Solve It Gracefully and Reliably
Imagine you built a booking system — for meeting rooms, appointments, or event seats — and suddenly you start getting complaints:
“Two people got booked into the same room at the same time!”
That, right there, is the infamous double booking problem. It’s one of those challenges that seems trivial until you deploy your system and real users start clicking “Book” at the same second.
In this blog, we’ll break down:
Why double booking happens
Why locking and threading tricks often fail
And how to solve it properly using C#, SQL, and smart design patterns — the same way large-scale systems like airline or hotel booking apps do it.
The Root Cause — Race Conditions
Double booking happens when two users try to reserve the same resource at nearly the same moment.
Let’s take an example:
John tries to book Meeting Room A for 10:00–11:00.
Ashley also tries to book the same room for 10:00–11:00.
Both apps check availability: “Room A is free.”
Both apps insert a booking record at the same time.
Result? Two confirmed bookings.
No exception. No warning. Just chaos.
That’s because in modern systems, reads and writes happen faster than you think, and if you don’t make the operation atomic (meaning — it happens as one unbreakable step), race conditions will occur.
Step 1: Understand the Core Principle
The solution starts with this golden rule:
“Only the database can truly prevent double bookings.”
Why?
Because databases can guarantee atomicity — once a transaction starts, no other transaction can interfere until it completes.
So even though you can add locks in C#, or check flags in memory, those don’t scale across servers or multiple processes.
We must move the protection closer to where the data lives — the database.
Step 2: Enforce Uniqueness at the Database Level
If your system deals with fixed slots (for example, 10:00–10:30, 10:30–11:00, etc.), the simplest and most powerful solution is to use a unique constraint.
For example, if you have a Bookings table:
CREATE TABLE Bookings (
Id UNIQUEIDENTIFIER PRIMARY KEY,
ResourceId UNIQUEIDENTIFIER NOT NULL,
SlotStartUtc DATETIME2 NOT NULL,
SlotEndUtc DATETIME2 NOT NULL,
UserId UNIQUEIDENTIFIER NOT NULL,
CreatedUtc DATETIME2 DEFAULT SYSUTCDATETIME()
);
CREATE UNIQUE INDEX UX_Bookings_Resource_Time
ON Bookings(ResourceId, SlotStartUtc, SlotEndUtc);
This line means:
You can’t have two bookings for the same resource, for the same time slot.
If two users try to insert at the same time, one will fail instantly with a unique constraint violation.
And that’s perfect — you can catch it in C# like this:
try
{
context.Bookings.Add(new Booking { ... });
await context.SaveChangesAsync();
}
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
{
Console.WriteLine(”Someone already booked this slot.”);
}
Simple, elegant, bulletproof.
Step 3: Handling Continuous Time Slots
But what if your system allows flexible durations — say 10:00–10:45 or 10:15–11:00?
Now the uniqueness constraint won’t work, because these intervals overlap without being identical.
So how do we solve that?
We ask the database to lock and check before inserting.
SQL Approach
BEGIN TRANSACTION;
-- Take a lock on potential overlapping records
IF EXISTS (
SELECT 1
FROM Bookings WITH (UPDLOCK, HOLDLOCK)
WHERE ResourceId = @resourceId
AND @startUtc < SlotEndUtc
AND SlotStartUtc < @endUtc
)
BEGIN
ROLLBACK;
RAISERROR(’Overlapping booking exists’, 16, 1);
RETURN;
END;
-- Otherwise, insert the new booking
INSERT INTO Bookings (Id, ResourceId, SlotStartUtc, SlotEndUtc, UserId)
VALUES (@id, @resourceId, @startUtc, @endUtc, @userId);
COMMIT;
The key part here is the locking hint:
UPDLOCKprevents others from reading or writing the same rows concurrently.HOLDLOCKensures the lock is held until the transaction completes.
Step 4: The Same Logic in C#
Here’s a practical C# implementation:
await using var conn = new SqlConnection(connectionString);
await conn.OpenAsync();
await using var tx = conn.BeginTransaction(IsolationLevel.Serializable);
var overlapCmd = new SqlCommand(@”
SELECT 1
FROM Bookings WITH (UPDLOCK, HOLDLOCK)
WHERE ResourceId = @rid
AND @startUtc < SlotEndUtc
AND SlotStartUtc < @endUtc”, conn, tx);
overlapCmd.Parameters.AddWithValue(”@rid”, resourceId);
overlapCmd.Parameters.AddWithValue(”@startUtc”, startUtc);
overlapCmd.Parameters.AddWithValue(”@endUtc”, endUtc);
var exists = await overlapCmd.ExecuteScalarAsync();
if (exists is not null)
{
await tx.RollbackAsync();
Console.WriteLine(”Overlapping booking found.”);
return;
}
var insertCmd = new SqlCommand(@”
INSERT INTO Bookings (Id, ResourceId, SlotStartUtc, SlotEndUtc, UserId)
VALUES (@id, @rid, @startUtc, @endUtc, @uid)”, conn, tx);
insertCmd.Parameters.AddWithValue(”@id”, Guid.NewGuid());
insertCmd.Parameters.AddWithValue(”@rid”, resourceId);
insertCmd.Parameters.AddWithValue(”@startUtc”, startUtc);
insertCmd.Parameters.AddWithValue(”@endUtc”, endUtc);
insertCmd.Parameters.AddWithValue(”@uid”, userId);
await insertCmd.ExecuteNonQueryAsync();
await tx.CommitAsync();
Console.WriteLine(”Booking successful!”);
This ensures that even if two users attempt to book the same slot at the same time, one of them will safely fail.
Step 5: Add Safety Layers Around It
Real-world systems often add a few extra layers of protection on top of the database transaction. Let’s look at the most common ones:
🔹 Idempotency Keys
If users refresh the page or hit “Book” twice, you might get duplicate POST requests.
Use an Idempotency Key — a unique token per booking attempt.
Store it in the database, and if it’s reused, simply return the same result instead of creating a duplicate booking.
🔹 Temporary Holds
Just like an airline holds your seat for 10 minutes before payment, your app can “soft lock” a slot while the user is completing the checkout process.
If the user doesn’t confirm within a time limit, the hold expires.
🔹 Distributed Locks
In high-scale systems, when bookings are handled across multiple app servers, you can use Redis distributed locks.
These locks prevent multiple servers from modifying the same resource simultaneously.
However, always remember: distributed locks are helpful, not foolproof. The database constraint should still be your final defense.
Step 6: Think About Boundaries
While implementing booking logic, always define clear boundaries for what counts as an overlap.
For example:
Should 10:00–10:30 and 10:30–11:00 be allowed together?
Yes, if you treat time ranges as half-open intervals —[Start, End)
So, a booking from 10:00 to 10:30 occupies all time up to but not including 10:30.
This tiny detail can save you countless bugs later.
Step 7: The Real-World Analogy
Think of your database as a hotel receptionist.
When two guests walk in to book the same room, the receptionist:
Checks the list (existing bookings).
Locks the ledger while writing.
Confirms the booking for one guest.
Politely tells the other: “Sorry, that room’s taken.”
Your system should behave exactly the same way — politely but firmly.
Step 8: Putting It All Together
Here’s the final architecture for a reliable, production-grade booking system:
API Layer (C#) → Receives the booking request.
Idempotency Check → Ensure repeat requests don’t duplicate bookings.
Database Transaction (Serializable) → Locks and checks for overlaps.
Insert Booking → If no conflict, confirm booking.
Return 201 or 409 → Respond with either “Created” or “Conflict”.
That’s it — a clean, elegant solution that scales safely.
Key Takeaways
Double booking is a race condition, not a code typo.
Avoid handling it just in code; move validation into the database.
Use unique constraints for fixed slots, and serializable transactions for flexible time ranges.
Add idempotency and temporary holds for real-world resilience.
Always store times in UTC and treat slots as half-open intervals to prevent hidden overlaps.
Final Thought
Preventing double booking isn’t about writing clever C# tricks.
It’s about designing your system so it behaves predictably under pressure.
In other words:
“Don’t try to race the user — design so both can win safely.”
By making your database the single source of truth and layering your logic smartly, you can guarantee that no two users will ever share the same seat, slot, or room — no matter how fast they click “Book Now.”


