Building Airbnb
Implement availability search, the hold-then-confirm booking with a database exclusion constraint, and async index propagation.
Availability search
Combine geo candidates with a date-range availability filter and other filters:
def search(area, check_in, check_out, filters):
candidates = geo_index.in_area(area) # geohash/quadtree (Yelp)
return rank([
l for l in availability_index.batch(candidates)
if l.available(check_in, check_out) # no overlap with booked ranges
and passes(l, filters)
])
The availability index is denormalized for fast filtering (eventually consistent with the source-of-truth calendar) — so re-check at booking time.
The booking guarantee: an exclusion constraint
The bulletproof defense against double-booking is a database constraint that physically forbids overlapping bookings — so even with a race, the second insert fails:
-- one booking per listing per night (Postgres exclusion constraint over a date range)
CREATE TABLE bookings (
listing_id bigint,
during daterange,
guest_id bigint,
EXCLUDE USING gist (listing_id WITH =, during WITH &&) -- && = ranges overlap → reject
);
No matter how the application races, the DB lets at most one booking exist for any overlapping range. This is the safety net under any application-level checks.
Hold → pay → confirm
A short hold reserves the dates during checkout; payment then confirms; failure releases:
def start_booking(listing_id, check_in, check_out, guest):
if not lease.acquire(f"hold:{listing_id}:{check_in}:{check_out}", ttl="10m"):
raise DatesUnavailable() # someone else is checking out
return checkout_session(listing_id, check_in, check_out, guest)
def confirm_booking(session):
try:
payment.charge(session) # take payment first
with txn():
insert_booking(session) # exclusion constraint enforces no overlap
except OverlapViolation:
payment.refund(session); raise DatesUnavailable() # lost the race → refund
finally:
lease.release(hold_key(session))
The lease gives good UX (dates held during checkout) and the constraint gives correctness (the real guarantee) — belt and suspenders.
Availability modeling
Store bookings as date ranges (compact) and check overlap with &&; the search index
materializes per-listing availability (or booked ranges) for fast filtering. The DB
calendar is the source of truth; the index trails it.
Async index propagation
When a booking or host calendar change lands, propagate to the search index asynchronously:
def on_booking_committed(booking):
index_queue.publish({"listing": booking.listing_id, "change": "booked",
"range": booking.during})
# indexer updates the availability index (eventually consistent)
A just-booked listing might appear in search for a moment — caught by the re-check at booking + the exclusion constraint.
Scale and failure handling
- Search (read-heavy) → geo + availability indexes, cached, replicated.
- Booking (consistency-critical) → transactional store, sharded by listing; the exclusion constraint is the ultimate guard.
- Payment fails / user abandons → hold expires (lease TTL), dates freed.
- Two simultaneous bookings → one wins the constraint, the other gets a clean “unavailable” + refund.
- Index lag → re-check availability at booking; never trust the index for the final decision.
- Hot listing (a popular property on a holiday) → contention on its calendar rows; the constraint serializes correctly; holds reduce wasted payment attempts.
The takeaway
Concrete signals: geo + date-range availability search (eventually consistent), a hold (lease) → pay → confirm flow, and a database exclusion/unique constraint as the unbreakable no-double-booking guarantee, with async index propagation. Splitting fast eventual search from a strongly-consistent booking transaction is the reusable pattern for any inventory/reservation system — which is exactly Ticketmaster next.