How would an Airbnb backend developer design a system to manage concurrent booking requests for a single property, preventing overbooking and ensuring data consistency?

AirbnbBackend Developer3–5 YearsConcurrency

Preventing overbooking in a high-concurrency booking system like Airbnb requires a robust design centered on transactional integrity and careful resource locking. The fundamental approach involves using database transactions to encapsulate the booking logic, ensuring atomicity, consistency, isolation, and durability (ACID properties). Within this transaction, we must acquire a lock on the property’s availability status to prevent other concurrent requests from observing stale data and booking the same slot.

Core concurrency strategy

For handling concurrent booking requests, a combination of optimistic and pessimistic locking can be employed. Pessimistic locking, specifically a row-level lock within a database transaction, is critical. When a user attempts to book a property for specific dates, the system initiates a transaction. It first checks if the property is available for those dates. If available, it acquires a database lock on the relevant availability record, for example, using SELECT FOR UPDATE. This lock prevents other transactions from modifying or reading the same record until the current transaction commits or rolls back. Once the lock is held, the system re-verifies availability, updates the availability status to booked, and creates the booking record. The transaction then commits, releasing the lock. For broader concurrency control, an optimistic locking mechanism can also be used, where a version number or timestamp is checked before an update, and the transaction fails if the version has changed, prompting a retry.

Best practice

To optimize performance and minimize lock contention, keep transactions as short and focused as possible. Index critical columns used in availability checks, such as property_id and date, to speed up queries. Implement idempotency for booking requests, so retrying a failed booking attempt does not result in duplicate bookings. For distributed microservices, consider using a distributed lock manager, like Redlock with Redis or Zookeeper, for scenarios where a single database lock is insufficient or not feasible across service boundaries, though this adds complexity and should be used judiciously.

Edge case interviewers probe for

Interviewers might ask about deadlocks, where two transactions are waiting for each other’s locks. This can be mitigated through proper lock ordering, setting timeouts for lock acquisition, and database deadlock detection/resolution mechanisms. Another edge case is handling partial failures in a distributed system, for example, if the booking is confirmed but payment fails. This requires compensation transactions or sagas to ensure eventual consistency and revert any reserved resources. They might also ask about the trade-offs of different isolation levels, such as Read Committed versus Repeatable Read, and their impact on performance and data consistency for this specific problem.

Common mistake

A common mistake is relying solely on application-level locks, which are ineffective in distributed environments with multiple instances of a service. Another is using overly broad or long-held locks, which drastically reduce system throughput and increase contention. Not handling retries gracefully, leading to duplicate bookings or user frustration, is also a frequent oversight. Developers sometimes forget to re-verify state after acquiring a lock, assuming the initial check is sufficient, which can lead to race conditions if the lock was acquired after the initial read but before the write.

What the interviewer is checking

The interviewer is assessing your understanding of fundamental database concepts, ACID, transactions, locking, distributed systems challenges, and practical solutions for ensuring data integrity under high load. They want to see if you can balance consistency requirements with performance considerations, identify potential failure modes, and design a resilient system. Your ability to reason about trade-offs and justify design choices, especially regarding consistency models and fault tolerance, is key.

Imagine a very popular local library that only has one copy of a highly anticipated new book. Many people want to check it out at the exact same moment. If the librarian just lets everyone try, two people might walk away thinking they have the book, leading to a big mess. To prevent this, the librarian uses a simple rule: when someone asks for the book, they physically put a “Reserved” sign on its spot on the shelf before checking it out to that person. No one else can even touch the book until it’s either checked out or put back.

In our Airbnb booking system, the “Reserved” sign is like a database lock on the specific dates for a property. When a booking request comes in, our system acts like the librarian. It immediately puts a “Reserved” sign on those dates in the database, so no other booking request can touch them. Only once that first booking is fully confirmed, the book is checked out, or if it fails, the book is put back, is the “Reserved” sign removed. This ensures only one person ever successfully checks out the “book”, books the property, at a time, preventing anyone else from thinking it’s available when it’s not.

Why interviewers ask this

Interviewers ask this to evaluate your understanding of distributed systems, database transaction management, and your ability to design resilient systems that handle high load and maintain data integrity. It’s a classic concurrency problem with real-world implications.

What a strong answer signals

A strong answer demonstrates a deep understanding of ACID properties, various locking mechanisms, optimistic versus pessimistic, and practical considerations like idempotency and error handling. It also signals your ability to think critically about trade-offs between consistency, availability, and performance.

Common follow-ups

  • How would you handle this scenario if your booking service was distributed across multiple data centers?
  • What are the trade-offs between strong consistency and eventual consistency in this context, and when might you relax consistency?
  • Describe how you would use a message queue in this booking flow, and what benefits or challenges it introduces for concurrency.

Advanced variation

An advanced variation might involve designing a system that handles dynamic pricing updates concurrently with booking requests, where the price might change mid-transaction. Another could be booking multiple interdependent resources simultaneously, like a tour package that includes flights and accommodation.

Consider a scenario where the last available room in a popular Airbnb property for a specific weekend is being viewed by five different users simultaneously. Each user clicks “Book Now” at nearly the same instant. Without proper concurrency control, it’s possible for multiple users’ requests to proceed past an initial availability check, leading to three or four confirmed bookings for a single room. A robust system using database row-level locking ensures that only the first successful transaction to acquire the lock and update the availability status can proceed, making that room unavailable before others can mistakenly book it. Other users’ attempts would then fail, informing them that the property is no longer available.

booking_service.py
# Pseudocode for a transactional booking logic with pessimistic locking

def book_property_dates(property_id, start_date, end_date, user_id):
    try:
        # Start a database transaction
        db.begin_transaction()

        # Acquire an exclusive lock on the availability record(s) for the dates
        # SELECT FOR UPDATE locks the selected rows until the transaction ends.
        availability_records = db.execute(
            "SELECT * FROM PropertyAvailability WHERE property_id = ? AND date BETWEEN ? AND ? FOR UPDATE",
            property_id, start_date, end_date
        ).fetch_all()

        # Re-check availability AFTER acquiring the lock
        if not all_dates_available(availability_records):
            raise BookingError("Property not available for selected dates.")

        # Update availability to 'booked'
        db.execute(
            "UPDATE PropertyAvailability SET status = 'booked', booked_by_user_id = ? WHERE property_id = ? AND date BETWEEN ? AND ?",
            user_id, property_id, start_date, end_date
        )

        # Create the booking record
        booking_id = db.execute(
            "INSERT INTO Bookings (property_id, user_id, start_date, end_date, status) VALUES (?, ?, ?, ?, 'confirmed')",
            property_id, user_id, start_date, end_date
        ).last_insert_id()

        # Commit the transaction, releasing the lock
        db.commit_transaction()
        return booking_id

    except Exception as e:
        # Rollback on error, releasing the lock and reverting changes
        db.rollback_transaction()
        print(f"Booking failed: {e}")
        raise
User 1 User 2 User 3 Book Request Book Request Book Request Booking Service (App Instances) Lock/Transaction Database (Availability Table) Update Status Success Failed (Lock Contention) Failed (Lock Contention)
  1. 1Database transactions with ACID properties are fundamental for ensuring data consistency in concurrent booking systems.
  2. 2Pessimistic locking, via SELECT FOR UPDATE, is crucial for preventing overbooking by exclusively locking records during critical updates.
  3. 3Optimistic locking, using version numbers, offers an alternative for reducing contention but requires retry logic.
  4. 4Keeping transactions short, implementing idempotency, and thorough error handling are best practices for high-performance concurrency.
  5. 5Distributed locks or sagas are necessary for managing concurrency across microservices or distributed environments.