Two guests open the same listing at the same second. Both see the same free week in June. Both tap Reserve. Only one of them can win. Get this wrong and you have an angry guest, a refund, and a host who trusts you less. This is the single hardest correctness problem in a lodging marketplace, and it hides behind a search box that looks simple.
The core problem
Booking is a distributed race condition. The read path (search) and the write path (reserve) want opposite things. Search wants to be fast, cached, and eventually consistent across millions of listings. Reserve wants to be strongly consistent for one listing on specific dates. You cannot serve both with the same storage strategy, so you split them.
A naive design stores availability as a boolean per listing and checks it in application code: read "is it free", then write "now it is booked". Those two steps are not atomic. Between the read and the write, another request can slip in and read the same "free" value. Both proceed. You just double-booked.
Key design decisions
Model availability as date ranges, not a single flag. A reservation is a half-open interval [check_in, check_out). Two reservations conflict when their intervals overlap. Postgres can enforce this directly with an exclusion constraint using a GiST index over a daterange plus the listing id. The database rejects the second overlapping insert. No application-level check, no race.
Push the conflict check into the database, not the app. Application-level "check then set" is where double-booking lives. Whether you use an exclusion constraint, a unique constraint on (listing_id, date) with one row per night, or SELECT FOR UPDATE on the listing row, the winner is decided by the database inside a single transaction. Pick one and let it be the source of truth.
Separate the search index from the booking store. Search runs against a denormalized index (Elasticsearch or a read replica) filtered by location, dates, price, and guest count. That index can lag by seconds and nobody cares, because search results are a hint, not a promise. The promise is made only when the transaction commits against the primary store.
The trade-offs
Row-per-night is simple to reason about and makes "which nights are free" a trivial query, but a long stay writes many rows and hot listings create lock contention on popular dates. The daterange exclusion constraint is compact and elegant, but it ties you to Postgres and its GiST support, and a rejected insert is less obvious to a new engineer debugging it.
SELECT FOR UPDATE serializes writers on a listing, which is correct but limits throughput per listing. For a marketplace that is usually fine, because contention is per property, not global. You are not serializing the whole site, only the handful of people fighting over one cabin.
There is also the pending-hold question. Payment takes a few seconds and can fail. If you commit the booking before payment clears, a failed charge leaves a phantom reservation. If you wait for payment, a slow processor lets a competitor grab the dates. The common answer is a short-lived hold: reserve the dates in a pending state with an expiry, confirm on payment success, and let a background job release expired holds. The hold itself must go through the same conflict check, or you have just moved the race.
How the real system does it
Large lodging platforms keep booking on a strongly consistent relational store and treat search as a separate, replicated, denormalized read path. Availability is intervals, conflicts are enforced by the store rather than trusted to application code, and money moves through a saga: reserve, authorize payment, confirm, or compensate by releasing the hold. Idempotency keys on the reserve endpoint stop a double-tapped button or a client retry from creating two bookings.
The lesson that carries to other systems: when a read is allowed to be stale but a write must be exact, do not force them to share a consistency model. Give search speed and give booking correctness, and let a transaction be the only place the truth is decided.
I wrote the full breakdown, with diagrams and the data model, here: https://www.systemdesign.academy/interview/design-airbnb
Top comments (0)