Two Guests, One Room, Zero Availability — A Concurrency Problem That Breaks Hotel Bookings
You’re building a room booking system. 10 rooms, multiple users booking simultaneously. What could go wrong?
Two Guests, One Room, Zero Availability — A Concurrency Problem That Breaks Hotel Bookings
You’re building a room booking system. 10 rooms, multiple users booking simultaneously. What could go wrong?
Everything. Let me show you exactly how, step by step, with the exact thread interleavings that cause each bug.
The Setup
An Airbnb-style booking manager. 10 rooms (IDs 0–9), all starting as “AVAILABLE”. You implement three methods across three levels of complexity:
book_room(room_id, guest_name)— mark a room as bookedcancel_booking(room_id, guest_name)— only the owning guest can cancelexecute_bundle_booking(room_a_id, room_b_id, guest_name)— book two rooms or neither
Each one introduces a new class of concurrency bug. Let’s walk through them.
Bug #1: The Silent Overwrite
The simplest requirement: book_room(room_id, guest_name). If the room is “AVAILABLE”, set it to “BOOKED BY Alice” and return true. If already booked, return false.
Here’s the interleaving that breaks a naive implementation:
Thread A: reads room 3 → "AVAILABLE"
Thread B: reads room 3 → "AVAILABLE"
Thread A: writes "BOOKED BY Alice"
Thread B: writes "BOOKED BY Bob"
Alice booked room 3 first. But Bob’s write came second and overwrote her booking. Alice shows up at the hotel and the room belongs to Bob. Neither thread received an error. The system thinks everything is fine.
This is the classic lost update — two threads read the same state, make independent decisions, and one silently obliterates the other.
The fix:
import threading
def __init__(self):
self.lock = threading.Lock()
self.rooms = {i: "AVAILABLE" for i in range(10)}
def book_room(self, room_id, guest_name):
with self.lock:
if self.rooms[room_id] != "AVAILABLE":
return False
self.rooms[room_id] = f"BOOKED BY {guest_name}"
return True
The availability check and the status write happen inside the same lock. No thread can read “AVAILABLE” while another is mid-write. Either you see the room is free and claim it, or you see someone else already did.
Bug #2: The Cancel Race
Now add cancellation: cancel_booking(room_id, guest_name). Only Alice can cancel Alice’s booking. If room 3 is “BOOKED BY Alice”, reset to “AVAILABLE” and return true. Otherwise return false.
The new race:
Thread A: cancel_booking(3, "Alice") — reads "BOOKED BY Alice" ✓
Thread B: book_room(3, "Charlie") — reads "BOOKED BY Alice", returns False
Thread A: writes "AVAILABLE"
Thread B checked room 3 while Alice’s cancellation was in progress. It saw “BOOKED BY Alice” and gave up. But a millisecond later, the room was free. Charlie missed an available room because the cancellation hadn’t committed yet.
In a high-traffic system, this means lost revenue. Rooms sit “available” but every concurrent booking attempt returns false because they all read the pre-cancellation state.
The fix is the same lock:
def cancel_booking(self, room_id, guest_name):
with self.lock:
if self.rooms[room_id] != f"BOOKED BY {guest_name}":
return False
self.rooms[room_id] = "AVAILABLE"
return True
The ownership check (!= "BOOKED BY Alice") and the reset (= "AVAILABLE") are one atomic operation. No thread can observe the room between these two steps.
Bug #3: The Deadlock Chain
Now the big one: execute_bundle_booking(room_a_id, room_b_id, guest_name). Book two rooms atomically — both must be “AVAILABLE”, or neither gets booked. All-or-nothing.
The test that breaks it: 10 threads, each calling execute_bundle_booking(i%10, (i+1)%10, f"Guest_{i}").
- Thread 0 wants rooms (0, 1)
- Thread 1 wants rooms (1, 2)
- Thread 4 wants rooms (4, 5)
- Thread 9 wants rooms (9, 0)
Now, if you decided to be clever and use per-room locks for performance (one lock per room instead of one global lock), here’s what happens:
Thread 0: locks room 0 ✓ (wants rooms 0, 1)
Thread 1: locks room 1 ✓ (wants rooms 1, 2)
Thread 2: locks room 2 ✓ (wants rooms 2, 3)
...
Thread 9: locks room 9 ✓ (wants rooms 9, 0)
Thread 0: tries to lock room 1 — BLOCKED (Thread 1 holds it)
Thread 1: tries to lock room 2 — BLOCKED (Thread 2 holds it)
...
Thread 9: tries to lock room 0 — BLOCKED (Thread 0 holds it)
A perfect circular chain. All 10 threads hold one lock and wait for the next. Nobody can proceed. Complete deadlock. Your application hangs. Requests time out. Users see a loading spinner forever.
The fix — keep the global lock:
def execute_bundle_booking(self, room_a_id, room_b_id, guest_name):
with self.lock:
if self.rooms[room_a_id] != "AVAILABLE":
return False
if self.rooms[room_b_id] != "AVAILABLE":
return False
self.rooms[room_a_id] = f"BOOKED BY {guest_name}"
self.rooms[room_b_id] = f"BOOKED BY {guest_name}"
return True
Wait — isn’t a global lock “too coarse”? For 10 rooms, it’s the right call. A global lock on a small resource set costs nanoseconds of contention. Per-room locks with ordering cost code complexity and debugging nightmares. Engineering is about matching the tool to the scale.
The Design Tradeoff Worth Thinking About
Here’s the interesting comparison. In a different problem on the same platform — a bank transfer system — you DO need per-account locks with lock ordering. Why? Because that problem adds a get_total_bank_assets() method that sums all balances consistently while transfers run. The global lock there would serialize all transfers, killing throughput.
But here? We’re checking two booleans and writing two strings. The global lock is correct, simpler, and faster than managing per-room locks with an ordering protocol.
The principle isn’t “always use lock ordering” or “always use a global lock.” It’s: match your synchronization strategy to your consistency requirements and your resource scale.
What I Learned
This problem changed how I think about three things:
1. Atomicity is about what observers can see. The cancel race isn’t about data corruption — it’s about a concurrent reader seeing an intermediate state that leads to a wrong decision.
2. Deadlocks aren’t just about obvious lock conflicts. The 10-thread circular chain isn’t something you’d catch in code review. It only manifests under specific scheduling. You need a test harness that forces these interleavings.
3. Simpler synchronization is often correct. My first instinct was per-room locks for “better performance.” That instinct created a deadlock. The boring global lock was the right answer.
Try It Yourself
The problem is “Airbnb Booking: The Deadlock Stay” on asynctote.com — concurrency track. The platform uses a deterministic scheduler that forces the exact interleavings described above — it doesn’t hope for bugs, it manufactures them.
If your solution survives the scheduler, it’s correct. If it doesn’t, you’ll see exactly which thread ordering broke it.
메타데이터
- post_id
- 09200e1587f7
- slug
- two-guests-one-room-zero-availability-a-concurrency-problem-that-breaks-hotel-bookings-09200e1587f7
- url
- https://medium.com/@asynctote/two-guests-one-room-zero-availability-a-concurrency-problem-that-breaks-hotel-bookings-09200e1587f7
- canonical_url
- https://medium.com/@asynctote/two-guests-one-room-zero-availability-a-concurrency-problem-that-breaks-hotel-bookings-09200e1587f7
- author_url
- https://medium.com/@asynctote
- status
- ok
- fetched_at
- 2026-06-09 15:37:30