Design Airbnb

Design a home-rental marketplace. Hosts publish listings with photos, a nightly price and a calendar of the nights they are open. Guests search by place, dates and number of guests, browse results on a map, and book a stay that is paid up front.

Search is busy and forgiving: a result that shows a home which turns out to be taken a minute later is annoying but tolerable. Booking is rare and unforgiving: two guests must never end up holding the same night at the same home, even when they click Reserve in the same second for a popular cabin on New Year's Eve.

In 45 minutes, settle the requirements and scale, design the search path and the booking path separately, and go deep on how availability is stored and how you stop double bookings without making search slow.

Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.

Design Path · Location & Marketplaces · problem 8 of 18

Ask before you design. Try answering each yourself, then compare.

  1. Is booking instant, or does the host have to approve each request?
    Typical answer

    Assume instant book for the core design; host approval is a state in the same booking flow and can be a follow-up.

  2. What scale are we designing for?
    Typical answer

    About 7 million active listings, 20 million daily searchers and around 1 million bookings a day, with holiday peaks about 3x.

  3. How fresh must availability be in search results?
    Typical answer

    Within a minute or so. The booking step re-checks against the source of truth, so search can be slightly stale.

  4. Do we handle payments ourselves?
    Typical answer

    No. We call an external payment provider to authorise and capture; we only store references to its transactions.

  5. Can a listing be booked for partial nights or by the hour?
    Typical answer

    No. The unit is one night; a stay is a contiguous range of nights from check-in up to (not including) check-out.

  6. Do prices change per night?
    Typical answer

    Yes, hosts can set per-night prices and minimum stays, but dynamic pricing algorithms are out of scope.

  7. Do hosts sync calendars from other rental sites?
    Typical answer

    Yes, through periodic calendar imports. Treat those as just another writer to the calendar.

Search and booking have wildly different volumes and correctness needs. Should they read from the same store?
How would you model availability so that 'is this home free for these 5 nights' is one cheap check?
Two guests click Reserve at the same instant for overlapping dates. Where exactly does one of them lose?
Payment takes seconds and can fail. What happens to the nights while you wait for it?
How does a booking made at 10:00:00 stop showing up as available in search?

Requirements

Functional

  • Hosts create and edit listings, photos, nightly prices and an availability calendar.
  • Guests search by location (or map bounds), check-in/check-out dates, guest count and filters, sorted by relevance.
  • Guests view a listing with its calendar and total price for the chosen dates.
  • Guests book a stay; the stay is held while payment runs, then confirmed or released.
  • Guests and hosts can cancel according to the listing's policy.

Non-functional

  • Zero double bookings: a night of a listing belongs to at most one confirmed or held stay.
  • Search p95 under 300 ms at ~3,500 queries per second at peak.
  • Booking p99 under 2 s excluding the payment provider; booking path 99.95% available.
  • Availability in search is at most ~60 s stale; booking always checks the source of truth.
  • Photos load quickly worldwide (served from a CDN).

Out of scope

  • Reviews, messaging between guests and hosts, and identity verification.
  • Dynamic pricing and ranking model training (we consume a ranking score).
  • Host payouts and tax calculation.

Back-of-the-envelope estimates

  • Search: 20 M searchers × ~5 searches ≈ 100 M queries/day ≈ 1,200 QPS on average and about 3,500 QPS at holiday peaks. Each query filters millions of listings by geography, dates and guests.
  • Bookings: ~1 M/day ≈ 12 per second on average, maybe 40 per second at peak. Tiny volume, but each one is a multi-row transaction plus a payment call.
  • Calendar rows: 7 M listings × 365 nights open ahead ≈ 2.6 B rows. At ~40 bytes each that is ~100 GB, comfortable for a sharded relational store.
  • Bookings history: 1 M/day × 2 KB × 365 × 5 years ≈ 3.7 TB, ~11 TB with three replicas.
  • Photos: 7 M listings × 25 photos × ~300 KB in several sizes ≈ 50+ TB in object storage, served by a CDN.

The design is driven by the gap between the two paths: search is about 100× the booking rate and tolerates staleness, while booking is low volume and needs strict correctness. That argues for a separate, denormalised search index fed asynchronously from a transactional booking store.

API

GET  /v1/search?bbox|place=..&checkIn=..&checkOut=..&guests=2&cursor=..
       -> {results:[{listingId, title, photo, totalPrice, lat, lng}], nextCursor}
GET  /v1/listings/{id}?checkIn=..&checkOut=..   -> listing, calendar, quote
POST /v1/bookings   Idempotency-Key: <uuid>
       {listingId, checkIn, checkOut, guests, quoteId, paymentMethodId}
       -> 202 {bookingId, status: HELD}
GET  /v1/bookings/{id}                           -> {status: HELD|CONFIRMED|FAILED|CANCELLED}
PUT  /v1/listings/{id}/calendar  {ranges:[{from, to, open, price}]}   (host)

Booking takes an idempotency key and returns a booking id right away, so a guest who double-taps or retries after a timeout gets the same booking instead of a second hold or a second charge.

Data model

Booking store (PostgreSQL, sharded by listing_id): calendar_night(listing_id, night, status, booking_id, hold_expires_at, price) with primary key (listing_id, night), and booking(booking_id, listing_id, guest_id, check_in, check_out, status, amount, payment_ref, idempotency_key UNIQUE). Sharding by listing keeps every row a booking touches on one shard, so the reserve step is a single-shard ACID transaction with no distributed locking.

Listing store: listing details, amenities and photo URLs in the same relational cluster (or a document store); photos live in object storage behind a CDN.

Search index (Elasticsearch/OpenSearch): one document per listing with a geo-point, capacity, amenities, price summary, ranking score and a compact availability field, for example a bitmap of the next 365 nights. It is a derived copy, rebuilt from change events, never written by the booking path directly.

Guest-side views: a bookings_by_guest table (sharded by guest) built from events, so 'my trips' does not scatter across listing shards.

High-level design

Search path. The guest app calls the API gateway, which routes to the search service. The search service turns a place name into a bounding box, then sends one query to the search index: geo filter on the box, capacity at least the guest count, and a check that the requested nights are all set in the listing's availability bitmap. It ranks the matches, computes a total price for the stay and returns a page. Identical popular queries ("Lisbon, this weekend, 2 guests") are cached in Redis for 30–60 seconds. Photos come straight from the CDN.

Booking path. The guest taps Reserve. The booking service checks the idempotency key, then runs one transaction on the listing's shard that moves every night of the stay from available to held, with a 10-minute expiry. If any night is not free, the transaction fails and the guest sees "just booked". Next it authorises the card with the payment provider. On success it flips the nights to booked and the booking to confirmed; on failure it releases them. A sweeper releases holds whose expiry passed, which covers crashed requests.

Keeping search fresh. Every committed change to the calendar is captured from the database log onto a change stream. An index updater consumes it and rewrites the availability bitmap of the affected listing, normally within a few seconds. Host edits to listings and prices follow the same route.

Open the HLD board and press Reference to see the architecture diagram.

Deep dives

Preventing double bookings

Store availability as one row per listing per night, and make the hold an all-or-nothing transaction on the listing's shard: UPDATE calendar_night SET status='held', booking_id=$b, hold_expires_at=now()+'10 min' WHERE listing_id=$l AND night >= $in AND night < $out AND (status='available' OR (status='held' AND hold_expires_at < now())), then commit only if the row count equals the number of nights; otherwise roll back. The row locks taken by the update serialise two overlapping attempts, so exactly one sees the full count. An alternative is a range table with an exclusion constraint on overlapping date ranges per listing, which lets the database itself reject overlaps. Either way, correctness lives in the database, not in a cache or in application memory.

Searching by dates quickly

Filtering 7 M listings by 'free for all of 12–17 March' cannot hit the booking database. Denormalise availability into the search document as a 365-bit bitmap (46 bytes) indexed from today, and check it with a script filter or, faster, precompute a few common facets such as 'free next weekend'. Apply the cheap geo and capacity filters first so the date check runs on thousands of candidates, not millions. Shift the bitmap each night with a batch job. Accept that the index lags by seconds: the listing page and the booking step re-read the source of truth, and a stale result just produces a polite 'no longer available'.

Holds, payments and expiry

A payment call can take several seconds, time out, or succeed after our request gave up. So the booking is a small state machine: HELD, then CONFIRMED or FAILED, with the idempotency key passed to the payment provider too. If our service crashes between authorising the card and confirming, a reconciler queries the provider for HELD bookings older than a minute and finishes or reverses them. Holds carry an expiry so an abandoned checkout frees the nights after 10 minutes; the confirm step re-checks that the hold is still ours, and if it lapsed and someone else took the nights, it voids the authorisation and tells the guest.

Hot listings and hot dates

A few listings (a famous treehouse, anything near a big festival) attract many simultaneous reservers. Because all their rows sit on one shard, contention is local and short: each hold transaction touches at most a few dozen rows and commits in milliseconds, so even 100 attempts per second on one listing queue briefly and most fail fast with 'just booked'. Do not add a distributed lock in front; it adds a failure mode without adding correctness. Rate-limit per listing at the gateway if bots appear.

Failure modes

  • If the search index or its updater falls behind, search shows stale availability; bookings stay correct because the hold transaction checks the database, and we alert on consumer lag.
  • If a booking-store shard primary fails, bookings for listings on that shard pause for the failover (tens of seconds); search keeps working and other shards are unaffected.
  • If the payment provider times out, the booking stays HELD; the reconciler later asks the provider for the result by idempotency key and confirms or releases.
  • If the booking service crashes after placing a hold, the sweeper releases the nights when the hold expires, so inventory is never stuck.
  • If the CDN has an outage for a region, photos fall back to the origin object store with lower cache hit rates; search and booking are unaffected.

Core algorithm in code

import random
import threading
from datetime import date, timedelta


def nights(check_in, check_out):
    d = check_in
    while d < check_out:
        yield d
        d += timedelta(days=1)


class ListingCalendar:
    """Per-listing night calendar. A stay [check_in, check_out) holds every night in between.

    In production this is a row per (listing_id, night) in the booking database, and the
    all-or-nothing check below is one transaction (or a conditional UPDATE that must touch
    exactly N rows). The lock here plays the role of that transaction.
    """
    def __init__(self, listing_id):
        self.listing_id = listing_id
        self.nights = {}          # date -> (status, booking_id, hold_expires_at)
        self.lock = threading.Lock()

    def _free(self, night, now):
        cur = self.nights.get(night)
        if cur is None:
            return True
        status, _, expires = cur
        return status == "held" and expires <= now      # an expired hold counts as free

    def try_hold(self, booking_id, check_in, check_out, now, ttl):
        with self.lock:
            stay = list(nights(check_in, check_out))
            if not all(self._free(n, now) for n in stay):
                return False
            for n in stay:
                self.nights[n] = ("held", booking_id, now + ttl)
            return True

    def confirm(self, booking_id, now):
        with self.lock:
            mine = [n for n, v in self.nights.items() if v[1] == booking_id]
            if not mine or any(self.nights[n][0] == "held" and self.nights[n][2] <= now for n in mine):
                return False                              # hold lapsed: payment must be refunded
            for n in mine:
                self.nights[n] = ("booked", booking_id, None)
            return True


cal = ListingCalendar("L-42")
start = date(2026, 12, 20)
winners = []


def guest(i):
    ci = start + timedelta(days=random.randint(0, 3))
    co = ci + timedelta(days=random.randint(2, 4))
    if cal.try_hold("B%02d" % i, ci, co, now=0.0, ttl=600.0):
        winners.append(("B%02d" % i, ci, co))


random.seed(7)
threads = [threading.Thread(target=guest, args=(i,)) for i in range(25)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("25 guests raced for overlapping stays; holds granted:")
for b, ci, co in sorted(winners, key=lambda w: w[1]):
    print("  ", b, ci, "->", co)
taken = [n for b, ci, co in winners for n in nights(ci, co)]
print("any night held twice?", len(taken) != len(set(taken)))
b0 = winners[0][0]
print("confirm", b0, "before expiry:", cal.confirm(b0, now=120.0))
if len(winners) > 1:
    b1 = winners[1][0]
    print("confirm", b1, "after the 10-minute hold lapsed:", cal.confirm(b1, now=900.0))

Trade-offs

  • A separate search index gives fast geo and date filtering but costs freshness, so the booking step must re-validate against the database.
  • Row-per-night calendars make overlap checks simple and lockable but cost billions of small rows; storing date ranges is compact but needs range-overlap constraints.
  • Sharding by listing makes holds single-shard transactions but costs a fan-out (or a derived view) for 'all bookings of a guest'.
  • Holding nights before payment avoids charging for a stay we cannot give, but costs inventory being locked by guests who abandon checkout, so the hold TTL must stay short.
  • Caching popular search pages for 60 s cuts index load but can show a home that was booked in the meantime.

What a strong answer shows

  • Splits a high-volume, stale-tolerant search path from a low-volume, strictly consistent booking path.
  • Models availability so overlap checks are atomic in the database (row-per-night with conditional update, or range exclusion constraint).
  • Uses a hold with expiry around the payment call and an idempotency key on the booking API.
  • Keeps the search index fresh through change data capture and accepts bounded staleness explicitly.
  • Chooses a partition key (listing) that keeps the critical transaction on one shard.
  • Backs design choices with numbers: ~3,500 search QPS vs ~40 bookings per second.

Mistakes to avoid

  • Checking availability and then writing the booking in two separate steps, which leaves a race window.
  • Using a Redis lock or in-memory flag as the only guard against double booking.
  • Querying the transactional calendar table for every search, which melts the database.
  • Charging the card before securing the nights, then having to refund on conflict.
  • Forgetting expiry on holds, so crashed checkouts block a listing forever.

Follow-ups to expect

  • How would you support request-to-book where the host has 24 hours to accept? (A PENDING_HOST state with a longer hold, plus a timer that auto-declines.)
  • How do you import calendars from other rental sites without overwriting our bookings? (Treat imports as blocks that only touch available nights, applied in the same transactional way.)
  • How would you rank results? (Retrieve candidates by filter, then rerank with a learned model using price, quality and conversion features.)
  • How do you handle a host cancelling a confirmed stay? (State transition plus refund via the provider and a re-open of the nights.)
  • How would you serve search in multiple regions? (Replicate the index per region; keep each listing's booking shard in one home region.)

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url