Marketplace & Booking
Design Airbnb
01
What the marketplace must do
What the marketplace must do
- Guests search by destination or map area, check-in and check-out dates and party size, with filters for price, room type and amenities.
- A listing page shows photos, house rules, reviews and a total price for the chosen dates including cleaning and service fees.
- Guests book instantly where the host allows it, or send a request the host must accept within 24 hours.
- Hosts create listings, set nightly prices and minimum stays, and block dates on their calendar.
- Calendars sync both ways with other booking sites through iCal feeds.
- Either side can cancel under the listing's policy, and refunds follow that policy automatically.
- No night is ever sold twice. This is the one rule that cannot bend.
- Search p99 under 400 ms; listing pages under 250 ms.
- A booking confirms or fails clearly within 5 seconds, and retries never charge twice.
- Search may show a listing that became unavailable seconds ago, as long as the booking step catches it.
- Booking stays available during a search outage, and search stays up if booking is degraded.
Ask whether inventory is a whole home, a room or a shared bed; one unit per night is far simpler than counted inventory. Ask whether prices vary per night, since that turns the quote into a sum over dates. And ask how strict the external calendar sync must be: iCal feeds are polled, so a double booking across two sites is something you can reduce but never fully prevent. Saying that out loud is a strong signal.
02
Sizing the load
Sizing the load
Assume 4.5 million active listings, 150 million nights booked a year and an average stay of just under four nights. Guests search many times for every booking, mostly in the evenings of their home time zone and heavily in January and early summer.
| Quantity | Working | Result |
|---|---|---|
| Bookings per year | 150M nights / 3.8 nights per stay | about 39M |
| Bookings per day | 39M / 365 | about 108K, 1.3 / s average |
| Peak booking rate | 8x average on a January evening | about 10 / s |
| Searches per day | assumption, about 550 per booking | 60M |
| Peak search rate | 60M / 86,400 x 3.5 | about 2,400 / s |
| Listing page views | about 4 per search | 240M a day |
| Calendar rows | 4.5M listings x 365 nights ahead | about 1.6B listing-nights |
| Calendar storage | 1.6B x about 40 B (state, price, booking id) | about 66 GB |
| Availability bitmap per listing | 365 bits | 46 B, about 210 MB for all listings |
| Photos | 4.5M x 25 photos x 4 sizes x 400 KB | about 180 TB |
Search is thousands of reads a second over data that may be a few seconds old. Booking is about ten writes a second that must be serialised per listing. Treat them as separate systems that share a source of truth, not one service that does both.
Every night of every listing for a year fits in about 66 GB. The booking database does not need heroic sharding; partitioning by listing id over a handful of Postgres primaries is enough for years.
03
API design
API design
04
Architecture overview
Architecture overview
flowchart LR
G["Guest app"] --> GW["API gateway"]
H["Host app"] --> GW
GW --> SR["Search service"]
GW --> LS["Listing service"]
GW --> QS["Quote and pricing service"]
GW --> BK["Booking service"]
SR --> ES[("Search index: geo + bitmap")]
LS --> LDB[("Listings DB + cache")]
BK --> CAL[("Calendar DB, partitioned by listing")]
QS --> CAL
BK --> PAY["Payment service"]
BK -. "calendar.changed" .-> K[["Kafka"]]
K --> IX["Indexer"]
IX --> ES
ICS["iCal sync workers"] --> CALThe calendar database is the source of truth for whether a night is free. Only two writers touch it: the booking service and the host calendar path (including iCal sync). Everything else reads it or reads a copy derived from it.
Search never queries the calendar directly. Each listing's document in the search index carries a 365-bit availability bitmap and per-night price bands. When the calendar changes, the booking service emits an event and the indexer patches that one document within a few seconds. A date-range filter becomes a bitwise check: are all bits between check-in and check-out set? That check is cheap enough to run on tens of thousands of candidates per query.
Prices shown in search are estimates. The quote service recomputes the exact total from the calendar's per-night prices, fees and taxes when the guest opens checkout, signs it and gives it a short expiry. The booking call carries that quote id, so the price the guest agreed to is exactly the price charged, even if the host edits prices a minute later.
Geo query by city or viewport, availability bitmap filter, then ranking by quality, price fit and conversion likelihood. Stale by seconds, by design.
Sums nightly prices, length-of-stay discounts, cleaning and service fees and local taxes. Returns a signed, expiring quote.
The only place that converts free nights into held and booked nights. Runs the saga with payment and talks to the calendar inside transactions.
Poll hosts' external calendars every 15 to 30 minutes and block nights that were sold elsewhere, and publish our own feed for other sites to read.
Core tables
| Table | Key | Holds |
|---|---|---|
listings | listing_id | Host, address, coordinates, capacity, amenities, policy ids, instant-book flag |
calendar_nights | listing_id, night | State (free, blocked, held, booked), nightly price, min stay, booking or hold id |
holds | hold_id | Listing, nights, guest, expires_at, attempt id |
bookings | booking_id | Guest, listing, date range, quote breakdown, policy version, payment refs, status |
booking_attempts | idempotency key | Current saga step, hold id, authorisation id, final response |
All of a listing's nights, holds and bookings live on the same partition, chosen by listing id. A booking transaction therefore never spans partitions, which is what keeps it short and safe. Guest-facing views such as "my trips" read from a separate index keyed by guest id that is fed by booking events.
05
Deep dive: selling each night once
Deep dive: selling each night once
Two guests press Book on the same flat for overlapping dates within the same second. Exactly one of them must succeed, and the other must get a clean 409 without a charge. Everything in this section is about making that true under failures.
Modelling nights
Store one row per listing per night: (listing_id, night, state, booking_id, price), with state one of free, blocked, held or booked. A stay is a set of consecutive rows. To take a stay, a transaction locks those rows with SELECT ... FOR UPDATE in night order and checks that every one is free. Locking in a fixed order means two overlapping attempts cannot deadlock; the second simply waits, then sees the nights are taken.
An alternative is one row per booking with a date range and a Postgres exclusion constraint: EXCLUDE USING gist (listing_id WITH =, stay WITH &&). The database itself rejects any overlapping range, which is elegant and hard to get wrong. Per-night rows win when prices, minimum stays and blocks also vary by night, which they do here, so the per-night table is the main store and the constraint can guard the bookings table as a second line of defence.
The booking saga
flowchart TD
S["POST booking with quote and idempotency key"] --> I{"Key seen before?"}
I -->|yes| R["Return stored result"]
I -->|no| T["Tx: lock nights, check free, mark held with 10 min expiry"]
T -->|conflict| C409["409 dates taken"]
T -->|ok| P["Authorise payment for quote total"]
P -->|declined| U["Tx: release held nights"]
U --> F["402 payment failed"]
P -->|authorised| B["Tx: held to booked, write booking row"]
B --> E["Emit booking.confirmed"]
E --> N["Notify host and guest, reindex listing"]Never hold a database transaction open while calling a payment provider. Their p99 can be several seconds, and a lock held that long would block every other guest looking at that listing. Instead, split the work into three short transactions around one external call. First, mark the nights held with an expiry and commit. Then authorise the card. Then flip held to booked and commit again.
Each step is recorded in a booking_attempts row keyed by the idempotency key, so a retry from the client or a restarted worker resumes at the right step instead of starting over. Authorisation uses the same key with the payment provider, so a retried call never creates a second charge. A sweeper releases any hold whose expiry has passed, which covers the case where the booking service dies between steps. If the sweeper finds an authorisation with no confirmed booking, it voids it.
Capture the money later. Authorise at booking, capture around check-in, and pay the host a day after check-in. That ordering makes cancellations cheap: before capture, a cancellation is a void rather than a refund.
Pricing the quote
A quote is a small pipeline. Read the nightly price for each night of the stay from the calendar rows, apply any weekly or monthly discount the host set, add the cleaning fee once, compute the guest service fee as a percentage of the subtotal, then add occupancy taxes, which depend on the listing's address and sometimes on the number of nights. Currency conversion happens last, at a rate fixed for the life of the quote.
Keep every rule versioned and record which version produced each quote. When a city changes its tourist tax on the first of the month, quotes issued the day before must still be honoured, and support needs to explain the exact numbers a guest saw. Store the full breakdown on the booking row, not just the total, so refunds under a partial cancellation can be computed from the same lines.
Request to book
When the host must approve, hold the nights for up to 24 hours instead of 10 minutes and authorise the card at request time. Other guests see those dates as unavailable. If the host declines or the window lapses, the sweeper releases the nights and voids the authorisation. Long holds are a trade-off: they protect the guest who asked first but can block a listing for a day, so cap how many pending requests one guest can have open.
Keeping search honest enough
Search can show a listing whose last free night was booked two seconds ago. That is acceptable if the listing page and the quote both read the calendar directly and the booking transaction is the final check. What is not acceptable is search being stale by hours. Track indexer lag as a first-class metric, and when it grows, fall back to a quick calendar check on the top results before returning them.
A Redis lock per listing is a common suggestion. It adds a second source of truth that can disagree with the database after a failover, and its expiry can fire while a slow transaction is still running. Row locks inside the database that owns the data give the same serialisation with none of those gaps.
06
Decisions and their costs
Decisions and their costs
Prices, blocks and minimum stays are naturally per night, and locking a few rows in order serialises overlapping bookings with no extra infrastructure. Ten bookings a second is light work for this pattern.
Simple and strictly correctThe database rejects overlaps for you, which is very hard to break. Per-night pricing has to live elsewhere. Good as a second guard on the bookings table.
Excellent as a safety netDate filtering becomes a bitwise test per candidate, and a single document update keeps it fresh. Search can scale independently of the calendar database.
Fast filter, seconds of lagAlways exact, but every search would fan out to the booking database and couple their availability. A search spike could slow bookings.
Couples the two halvesShort transactions, no locks across network calls, clear compensation steps. Costs a sweeper and careful state tracking.
Survives partial failurePayment providers do not take part in your 2PC, and a coordinator failure leaves locks held. It solves the wrong problem.
Not available in practiceCancellations before capture are free voids instead of refunds, and the guest's card is not charged months early. Authorisations expire after about a week, so long-lead bookings re-authorise on a schedule.
Cheaper cancellationsSimpler money flow and no re-authorisation, but every early cancellation becomes a refund with fees and a delay for the guest.
Fine for short booking windows07
Failure modes
Failure modes
Both requests pass the search filter and both reach the booking service within milliseconds.
The row-lock transaction lets one mark the nights held; the other sees held nights and gets a 409 before any payment call. The exclusion constraint on bookings would reject a second confirmed range even if application code had a bug.The booking service crashes after the card is authorised but before nights flip to booked. The guest sees a spinner and a pending charge.
The attempt row records "authorised". On restart, or when the guest retries with the same key, the service resumes and confirms. If the hold has already expired and the nights were taken, it voids the authorisation and tells the guest clearly.A host's flat is booked on another site, but our iCal poll runs every 30 minutes. A guest books the same nights here in between.
Poll more often for hosts with many external bookings, encourage API-level channel integrations, and have a policy for this case: rebooking help and a credit for the guest. This cannot be fully prevented with polled feeds.A host drags in 40 RAW files of 60 MB each from a camera. Upload stalls, processing queues back up.
Upload directly to object storage with signed URLs and size limits, process asynchronously into a few standard sizes, and show the listing with placeholders until processing finishes.A festival announces dates and a million people search the same small town for the same weekend.
Cache the first results page per query for 30 to 60 seconds, add search replicas ahead of known events, and let ranking push already-held listings down.Competitors and rate-tracking services crawl every city every hour, doubling search load.
Rate-limit per account and per IP range, require sessions for deep pagination, and serve cached pages to obvious bots.08
How to handle the interview
How to handle the interview
Split search from booking in the first five minutes. Say one is a read-heavy, eventually consistent system and the other is a low-volume, strictly correct one. Interviewers grade on whether you see this.
Draw the night as the unit of inventory. Per-night rows make double booking a row-lock question. Mention the exclusion constraint as an alternative or a backstop.
Never call payments inside a lock. Hold, authorise, confirm, each in its own short step, with idempotency keys and a sweeper.
Explain how search gets availability. A bitmap per listing in the index, updated by events, gives fast date filters with bounded staleness.
Quote before you book. A signed quote fixes the price the guest agreed to and stops surprises when hosts change prices mid-checkout.
Admit what you cannot guarantee. Polled external calendars can conflict. Proposing a recovery policy shows judgement, not weakness.
09
Growing the design in stages
Growing the design in stages
Each step answers a specific limit. The booking database stays a plain relational store far longer than people expect.
Listings, calendar and bookings in one database. Search is a SQL query with a date join. Row locks already prevent double booking.
Move search to Elasticsearch with an availability bitmap, cache listing pages, put photos behind a CDN. Booking stays on Postgres.
Holds, signed quotes and idempotency keys formalise checkout. Calendar changes stream through Kafka to the indexer. The SQL date join in search is gone.
Calendar data is split across several primaries by listing id, search runs in every region, and pricing adds smart suggestions for hosts.
Watch and read
Further watching and reading
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.

