← Back to list

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?

Asynctote · 2026-05-03 13:22 · 0 claps · 4.0 min read
#concurrency #threads #low-level-design
Open on Medium ↗
Wiki topics: GEN · Genomics & Sequencing ✈️ · Travel

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:

  1. book_room(room_id, guest_name) — mark a room as booked
  2. cancel_booking(room_id, guest_name) — only the owning guest can cancel
  3. execute_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