SysDesignPrep.com
System design interview question

Design Airbnb

Search 7 M listings by place, dates and price, book a stay without ever selling the same night twice, and keep calendars in sync with hosts’ other platforms.

Last updated 2026-09-30. Difficulty: hard. Patterns: availability-search, inventory, double-booking, holds, calendar-sync. Reported at Airbnb, Amazon, Booking.com, Expedia, Uber.

Walk through a strong candidate's answer, turn by turn.

The interviewer asks, the candidate answers and draws, and you press Next. Pause to answer yourself at the key decisions, and ask the AI Mentor anything along the way.

Functional requirements

  • Search stays. By location (or map area), check-in and check-out dates, number of guests, price range and amenities. Only listings free for every night of the stay appear.
  • View a listing. Photos, description, calendar, the total price for the chosen dates, and reviews.
  • Book. Instant Book confirms immediately; Request to Book holds the dates for the host to accept within 24 hours. Payment is authorised at booking and captured later.
  • Hosts manage calendars and prices. Block dates, set nightly prices and minimum stays, and connect calendars from other platforms (iCal).
  • Cancel. Releases the nights and refunds according to the cancellation policy.
  • Out of scope. Messaging between guests and hosts, reviews beyond a mention, payouts to hosts, and the pricing model itself.

Non-functional requirements

  • No double bookings (zero). This is the requirement that drives the hardest trade-off: search can be seconds stale and still useful, but two guests must never both hold a confirmed booking for the same listing and night, even when they click at the same instant.
  • Search latency (p99 < 500 ms). Geo, guests, price and availability across a date range, ranked.
  • Scale (~1,200 searches/s avg · ~6 bookings/s avg). About 200 searches for every booking. Search is the volume; booking is the correctness.
  • Freshness (calendar changes in search within a minute). A listing shown as available that turns out booked frustrates guests; the booking step catches it, but it should be rare.
  • Payment correctness. A booking and its payment authorisation succeed or fail together; no confirmed stay without a payment, no charge without a stay.
  • Availability (99.95 %). Search can degrade (fewer filters, cached results); the booking path must stay correct before it stays fast.

Back-of-envelope estimates

  • Searches per second: ~1,200 avg · ~5,000 peak. 100 M searches a day ÷ 86 400 ≈ 1,160/s; travel planning peaks in evenings and after holidays, about 4× ≈ 5 k/s.
  • Bookings per second: ~6 avg · ~50 peak. 500 k bookings a day ÷ 86 400 ≈ 6/s. Tiny: the booking path can afford a strongly consistent transaction on every request.
  • Calendar rows: ~5 B. 7 M listings × 730 nights (two years ahead) ≈ 5 B listing-night rows in the source of truth, at ~50 B each ≈ 250 GB. Sharded by listing.
  • Availability as bitmaps: ~320 MB. One bit per listing per night: 7 M × 365 bits ÷ 8 ≈ 320 MB for a year ahead. Small enough to sit inside every search shard’s memory, which is what makes date-range filtering fast.
  • Candidate listings per search: ~5 k. A city search after geo and guest filters often matches 5 k listings; checking a 4-night range is 4 bit tests each, 20 k bit operations: microseconds.
  • Hold expiry: ~15 min. A booking in progress holds its nights for 15 minutes while the guest pays; abandoned holds release automatically, so dates are never stuck.

Components

  • Guest app: Searches, views listings and books. Sends an idempotency key with every booking attempt so a double tap or a retry never creates two bookings.
  • Host app: Hosts edit listings, nightly prices, minimum stays and blocked dates, accept or decline requests, and connect external calendars.
  • API gateway: Authentication, rate limiting (scrapers love listing data) and routing to search, booking and listing services.
  • Search service: Builds the query (area, guests, amenities, price), filters candidates by availability for every night in the range, prices the stay, ranks, and returns a page.
  • Search index (geo + attributes · sharded by region): One document per listing with location, capacity, amenities, base price and quality signals. Answers the non-date filters and returns a few thousand candidates.
  • Availability bitmaps (bit per night, in memory): For each listing, a bitmap of free nights for the next year (and the minimum-stay rules), kept in memory next to each search shard. A date-range check is an AND over a few bits.
  • Booking service: The correctness core: places holds on nights in one transaction, authorises payment, confirms or releases, and enforces the request-to-book state machine. Idempotent per guest key.
  • Calendar store (Postgres · (listing, night) unique · sharded by listing): The source of truth: one row per booked or blocked listing-night with its status (held, booked, blocked) and owner. A unique constraint on (listing, night) makes double booking impossible.
  • Payment provider (authorise · capture · refund): Authorises the guest’s card at booking and captures the charge later (often after check-in), so a declined host request or a cancellation is a voided authorisation, not a refund. See Design a Payment System.
  • Listing service: Listing details, photos, house rules, nightly price overrides and minimum stays. Writes go to the listing store and reach search through the change stream.
  • Listing store: Listings and their pricing rules. Read for listing pages and hydration; small compared with the calendar.
  • Change stream (CDC → Kafka): Every committed change to calendars and listings, in order per listing. Feeds the availability bitmaps and the search index within seconds.
  • Calendar sync (iCal import / export): Polls hosts’ calendars on other platforms every few minutes and blocks nights booked there; publishes our bookings as a feed they poll. The main source of double bookings across platforms.
  • Pricing engine: Suggests nightly prices from demand, seasonality and local events; hosts who opt in have prices updated nightly. Its output is just another listing change.

User flows

  1. Search: Paris, 12 to 15 October, 2 guests. Non-date filters narrow 7 M listings to a few thousand; in-memory bitmaps keep only those free for all three nights; then pricing and ranking.
    1. The guest searches with an area, dates, guests and a price cap.
    2. The index returns listings in the area that fit two guests and match the filters. Geo, capacity and amenity filters in one query; a cheap pre-score keeps the top few thousand. The index does not know about dates: availability changes too often to reindex documents for it.
    3. For each candidate, the search service checks the availability bitmap for the three nights and the minimum stay. Three bit tests per listing, all in memory: 5 k candidates take microseconds. Minimum-stay and check-in-day rules are stored alongside the bitmap and checked at the same time.
    4. Survivors are priced for the exact nights and ranked. Total price depends on each night’s price, length-of-stay discounts and fees, so it is computed per stay, not stored. Ranking blends relevance, quality, price and the guest’s booking likelihood.
    5. The page is returned; the result may be seconds stale, which the booking step will catch. Search reads replicas fed by the change stream, so a night booked a second ago can still appear free. That is acceptable because booking checks the source of truth.
  2. Book with Instant Book. The correctness path: hold the nights in one transaction against a unique constraint, authorise payment, confirm. Every step is idempotent.
    1. The guest confirms the stay; the request carries an idempotency key.
    2. The booking service inserts a held row for each night in one transaction. The unique constraint on (listing_id, night) does the hard work: if any night is already held, booked or blocked, the insert fails and the whole transaction rolls back. No locks are held across the payment call.
    3. It authorises the payment for the total. Authorise now, capture later: if anything after this fails, the authorisation is voided rather than refunded. The payment call uses the booking id as its idempotency key.
    4. On success the held rows become booked; on failure they are deleted. One update by booking id. If the service crashes between authorisation and confirmation, a sweeper finds holds past expiry, checks the payment state, and either confirms or releases. Nights can never stay held forever.
    5. The change reaches search within seconds. CDC publishes the committed rows, and each search shard flips the three bits for that listing. Holds also flip the bits, so a dated-out listing disappears from search while someone is paying for it.
  3. Two guests click Book on the same nights at the same moment. The contention case. Search showed both of them the listing as free; the database decides, and exactly one wins.
    1. Both requests reach the booking service within milliseconds. Both came from search results that were correct a second ago. Search staleness is expected; correctness is enforced here.
    2. Both transactions try to insert rows for 12, 13 and 14 October. Both writes go to the same shard (sharded by listing). The first to commit wins; the second gets a unique violation on the first conflicting night and rolls back completely, so it never holds a partial set of nights.
    3. The loser gets a clear "just booked" response and alternatives. The app shows similar listings for the same dates rather than an error page. No payment was authorised for the loser, because holding nights comes before payment.
    4. Overlapping but different ranges are handled the same way. A guest wanting 10 to 13 October and another wanting 12 to 15 conflict only on the night of the 12th: the per-night rows make partial overlaps conflict exactly where they overlap, with no range logic.
  4. The host’s other platform sells the same nights. The failure path that actually causes double bookings: calendars synced by polling. The design narrows the window and handles the conflict when it happens.
    1. A guest books the listing on another platform; it appears in the host’s iCal feed there. iCal feeds are plain files polled by each platform. There is no push and no transaction across platforms, so a window always exists.
    2. Calendar sync polls the feed every few minutes and blocks the nights here. The block is an insert with status "blocked" against the same unique constraint. If it succeeds, the nights are gone from search within seconds.
    3. If one of the nights was booked here in the meantime, the insert fails: a real cross-platform conflict. The system cannot resolve it alone, because one of the two bookings must be cancelled and only the host knows which. The host is alerted immediately; policy decides who rebooks whom.
    4. Our own bookings are exported as a feed the other platform polls. The window cannot be closed with polling. It is narrowed by polling often, and removed entirely when hosts use a channel manager with API integrations instead of iCal.
  5. A host changes prices and blocks dates. The background path: the listing and calendar stores are the truth; search follows them through the change stream.
    1. The host blocks a weekend and raises prices for a festival week.
    2. The pricing engine writes suggested nightly prices for hosts who opted in. A nightly batch; its output is just another set of listing changes, so it uses exactly the same path to search.
    3. Committed changes flow through CDC to the bitmaps and the index. Ordered per listing, so a block followed by an unblock never applies in the wrong order. Bitmaps update in milliseconds; index documents (base price, quality) within seconds.

Deep dives

Searching by dates

How do you find listings free for every night of a stay without checking billions of calendar rows?

The calendar is about 5 B listing-night rows. A search for four nights in Paris would, done naively, join thousands of candidate listings against their calendar rows for those nights: millions of row reads per search, over a thousand times a second.

Availability also changes constantly (every booking, hold, block and iCal sync), so putting it into the search documents means reindexing listings all day. The trick is that availability for a year is just 365 bits per listing, and all of it fits in memory.

  • Index for non-date filters, then in-memory availability bitmaps per listing, updated from CDC chosen
  • Store available dates in the search document and filter in the engine situational: small catalogs or low booking rates where reindexing is cheap
  • Join candidates against calendar rows in SQL rejected
  • Precompute "available windows" per listing rejected

The answer: The search index handles location, capacity, amenities and base price, returning a few thousand pre-scored candidates per query. Each search shard holds, in memory, a bitmap of free nights for the next 365 days for the listings it owns, plus minimum-stay and allowed check-in day rules, updated from the calendar change stream in milliseconds. The search service filters candidates with a bitwise check over the requested nights, then prices and ranks the survivors. Holds count as unavailable, so a listing being paid for vanishes from search. Search results may be seconds stale; the booking transaction is the only authority.

A guest searches with flexible dates ("any weekend in November"). How?

Turn the request into a small set of concrete ranges (each Friday to Sunday) and test each listing’s bitmap against each range, keeping listings free for at least one. It is still a handful of bit operations per candidate, and the result can say which weekends are free.

What happens to bitmaps when a shard restarts?

They are rebuilt from a snapshot of the calendar for that shard’s listings, then the change stream is replayed from the snapshot’s position. The shard reports itself not ready until the replay catches up, so it never serves results from a half-built bitmap.

How stale can search be, and how would you measure it?

Normally a second or two: the CDC lag plus applying the change. Measure it directly by stamping changes with their commit time and tracking the delay to the bitmap update, and indirectly by the rate of bookings that fail with dates_unavailable after a search showed the listing free.

How do you rank once you have the available listings?

A two-stage ranker: a cheap score in the index (relevance, quality, price) to pick candidates, then a learned model on the survivors predicting the chance this guest books. Price relative to the area and the dates matters a lot, so it is computed per stay before ranking.

Never selling a night twice

Two guests try to book overlapping stays at the same instant. How do you guarantee only one succeeds?

Check-then-write is the classic bug: both requests read "available", both write "booked". Any design where the check and the write are separate operations, even milliseconds apart, will double book under load.

Stays are ranges, but conflicts are per night: 10 to 13 October and 12 to 15 October overlap only on the night of the 12th. Representing each night as its own row turns range overlap into a simple uniqueness question the database can enforce.

  • One row per (listing, night) with a unique constraint; insert all nights in one transaction chosen
  • Optimistic concurrency on a per-listing calendar version situational: listings with very few bookings, or a document store without multi-row transactions
  • Distributed lock per listing around check-then-write rejected
  • Exclusion constraint on date ranges (Postgres GiST) situational: a single Postgres database with moderate scale

The answer: The calendar has one row per occupied listing-night (held, booked or blocked), with a unique constraint on (listing_id, night) and a booking id. Booking inserts all nights of the stay in one transaction; a conflict on any night aborts the whole thing, so a guest never holds part of a stay. The calendar is sharded by listing id, so every booking for a listing is a single-shard transaction. Free nights have no rows; availability is the absence of a row. The idempotency key on the request maps to the booking id, so retries cannot create a second set of holds.

Why insert rows for held nights before taking payment, rather than after?

Because payment is slow and can fail, and holding nothing while paying invites the race. Holding first, with an expiry, means the guest who pays is guaranteed the nights, and a guest who abandons checkout releases them automatically.

A host has the same flat listed twice (two listing ids). Can that double book?

Yes, because the constraint is per listing id. Model the physical unit: both listings point to one unit id, and the unique constraint is on (unit_id, night). Multi-unit hotels use the same idea with a count of rooms per night instead of a single row.

How does cancellation free the nights?

Delete the booking’s night rows (or mark them cancelled and exclude them from the constraint with a partial unique index), in the same transaction that changes the booking status. CDC then flips the bits back on, and the nights reappear in search within seconds.

What if the calendar shard for a listing is down?

That listing cannot be booked until it recovers or fails over to a replica, and the booking call fails fast with a retryable error. Other listings, on other shards, are unaffected. Correctness wins over availability on this path.

Holds, payments and request-to-book

Instant Book confirms at once; Request to Book waits up to 24 hours for the host. How do the nights and the money stay consistent?

A booking spans two systems that cannot share a transaction: our calendar and the payment provider. If nights are confirmed and the payment fails, the host loses a sale; if the card is charged and the nights fail, the guest pays for nothing.

Request to Book adds time: the nights must be held for up to a day while the host decides, the card must not be charged yet, and the hold must not outlive the request.

  • Hold nights with an expiry, authorise (not capture) payment, confirm; a state machine and sweeper resolve every interruption chosen
  • Charge immediately, refund on failure rejected
  • Two-phase commit across calendar and payment provider rejected

The answer: A booking moves through states: held → authorised → confirmed (Instant Book), or held → authorised → pending_host → confirmed or declined (Request to Book). The hold rows carry an expiry: 15 minutes for checkout, 24 hours for a host request. Payment is authorised with manual capture using the booking id as the idempotency key; capture happens around check-in, and declines, expiries and cancellations void the authorisation. A sweeper scans for holds past expiry and reconciles with the payment provider’s state before releasing or confirming, so a crash at any point resolves to a consistent outcome. Payments are covered in depth in Design a Payment System.

The service crashes right after the payment was authorised but before confirming. What happens?

The held rows stay until expiry. The sweeper then asks the payment provider about the booking’s payment (by idempotency key), sees an authorisation, and confirms the booking. If the provider shows nothing, it releases the nights. Either way, the outcome matches the money.

A guest books a stay ten months away. The authorisation expires in seven days. Now what?

Take a small deposit or store the payment method and charge closer to the date, re-authorising before check-in, with the booking state tracking it. If the later charge fails, the guest is notified and the booking is cancelled after a grace period, releasing the nights.

Why does the hold count as unavailable in search?

So other guests do not see a listing they cannot book for the next 15 minutes. If the hold is released, the bits flip back. This trades a little availability for far fewer failed bookings.

Calendars on several platforms

Hosts list the same flat on three platforms, synced by iCal. How do you avoid double bookings you cannot see?

iCal sync is a file each platform publishes and the others poll, every 15 minutes to a few hours. There is no transaction across platforms. A night booked elsewhere is invisible here until the next poll, and in that window a guest here can book it too.

This is not solvable with a better database: the other system is outside our control. The design goal is to shrink the window, detect conflicts immediately, and give the host a clear path to resolve them.

  • Frequent polling, blocks inserted against the same unique constraint, immediate conflict alerts; API integrations through channel managers where available chosen
  • Poll rarely and trust hosts to keep calendars updated rejected
  • Refuse external calendars rejected

The answer: Calendar sync polls each connected feed every few minutes (more often for listings with bookings soon) and inserts "blocked" rows for nights booked elsewhere, through the same unique constraint. A failed insert means a real conflict: both platforms sold the night. The host is alerted at once with both bookings, and the policy (usually the platform that confirmed first keeps the guest, the other is rebooked with help) is applied. Our own calendar is exported as a feed. Professional hosts are encouraged onto channel managers that use APIs with near-real-time updates, which removes the polling window.

How would you prioritise which feeds to poll most often?

By risk: listings with many upcoming free nights and a high booking rate, and feeds that changed recently. A listing booked out for months can be polled rarely; one with tomorrow free and high demand should be polled every minute or two.

How do you measure the damage?

Count conflicts detected by sync per thousand bookings, and the hours between a conflicting booking here and its detection. Both should fall as polling improves and as hosts move to API integrations.

Should an external block be able to cancel a booking made here?

Never automatically. The sync only inserts blocks for free nights; a conflict on an existing booking is surfaced to the host and support, because cancelling a guest's trip is a decision with policy, compensation and trust consequences.

How do you avoid importing your own bookings back from the other platform's feed?

Feeds can echo: the other platform imports our booking, then its export contains it. Each exported event carries our booking id in its UID; the importer recognises and skips events that originated here.

Nights, dates and time zones

What exactly is a "night", and why does it matter for correctness?

A stay from 12 to 15 October is three nights: the 12th, 13th and 14th. Check-out day is not a night, which is why one guest can leave on the 15th and another arrive the same day. Getting this wrong causes either false conflicts (no same-day turnover) or real double bookings.

Nights are local dates at the listing, not timestamps. A guest in Tokyo booking a flat in Lisbon for "12 October" means Lisbon’s 12 October, regardless of what time it is in Tokyo.

  • Store nights as plain local dates of the listing; check-out exclusive; convert only for display chosen
  • Store stays as UTC timestamp ranges rejected

The answer: A night is a DATE in the listing’s local calendar, and a stay is [check-in, check-out) with check-out exclusive. The calendar’s unique key is (listing_id, night DATE). Time zones only appear in rules about now: "same-day bookings until 18:00 local" and hold expiries use the listing’s zone. Prices are per night, so a stay’s price is the sum over its nights plus fees.

A guest wants to book tonight at 23:30 local time. Allowed?

Depends on the host’s same-day cut-off, evaluated in the listing’s time zone, not the guest’s or the server’s. That is the main place time zones matter, and it is a rule check before the hold, not part of the conflict check.

How do minimum stays interact with availability search?

A free three-night gap in a listing with a four-night minimum is not bookable. The bitmap check also enforces minimum stay (and allowed check-in days) so such listings do not appear in search only to fail at booking.

What if a listing changes time zone (data fix)?

Nights are stored as local dates, so the calendar itself does not change. Only now-based rules (cut-offs, expiries) shift. Treat it as a listing edit that flows through CDC like any other.

How do you price a stay that crosses a price change?

Sum the price of each night individually, then apply length-of-stay discounts and fees. Because nights are explicit, a stay that starts in low season and ends in high season is priced correctly without special cases.

The theory behind it

  • Distributed transactions and idempotency: Why cross-service transactions are hard, two-phase commit vs sagas, the outbox pattern, idempotency keys, exactly-once semantics, and how to keep money and inventory correct.
  • Search, indexing and autocomplete: Inverted indexes, analysis and ranking, prefix search with tries, filtering by permissions, index freshness, and when vector search actually belongs in the answer.
  • Data modelling for reads: Modelling from access patterns rather than entities: denormalisation, precomputed views, fan-out on write versus read, and the write amplification each choice buys you.

Related

Open in your browser to sign in

Google does not allow sign-in inside this app's built-in browser. Open this page in Safari and sign in there. The link opens this same page.

Tap the ⋯ or share button at the top or bottom of the screen, then Open in browser. Or copy the link and paste it into Safari.