How Airbnb works at scale
Short-term rentals look like a real-estate site with photos. At scale they are a marketplace: millions of listings that change price and availability by the night, search that must feel instant on a map, and a booking ledger where double-booking one weekend is a company-ending bug.
We work through the design in order—requirements first, numbers second, architecture third, APIs last—using an Airbnb-class product as the mental model, not any one company’s private implementation.
What you should be able to do after reading:
- Separate the three loops—inventory, discovery, booking—and assign the right database to each.
- List functional and non-functional requirements for guests, hosts, and the platform.
- Walk one reservation: search → quote → hold dates → pay → confirm → payout.
- Explain how calendars prevent double booking and how search indexes stay fresh.
- Read the technical section: REST flows, reservation states, and payment idempotency keys.
Step 0 — How we will work through the problem
Ordered thinking beats memorizing boxes. Use this sequence when you design a lodging marketplace:
- Clarify scope. Nightly stays only, or Experiences too? Instant Book vs host approval? International payouts in scope?
- Write requirements. Functional = guest/host behaviors. Non-functional = search latency, calendar correctness, payment integrity.
- Do napkin math. Listings count, searches per second, bookings per day—so nobody assumes one MySQL row stores the planet.
- Draw three loops before naming Elasticsearch or Stripe.
- Tell one story—a guest books three nights—then failure cases (calendar race, payment timeout, host decline).
flowchart TB
subgraph inv [Inventory loop]
H[Host calendar edits] --> CAL[Availability service]
CAL --> IDX[Search index refresh]
end
subgraph disc [Discovery loop]
G[Guest search] --> ES[Search + rank]
ES --> MAP[Map pins + listing cards]
end
subgraph book [Booking loop]
Q[Quote] --> HOLD[Date hold]
HOLD --> PAY[Payments]
PAY --> RES[Reservation state machine]
RES --> PAYOUT[Host payout]
end
IDX --> ES
MAP --> Q
Step 1 — Functional requirements (guests, hosts, platform)
Functional requirements are behaviors the product must ship. Group by actor so each row maps to a service later.
| Actor | Requirement | Why scale makes it hard |
|---|---|---|
| Guest — search | Location, dates, guests, filters; map + list views | Geo + facet queries over millions of listings |
| Guest — listing | Photos, amenities, house rules, reviews, host profile | CDN media; aggregated review scores |
| Guest — book | Price breakdown, taxes/fees, Instant Book or request | Quote must match calendar at commit time |
| Guest — trip | Itinerary, check-in instructions, messaging host | Post-booking async notifications |
| Guest — cancel | Policy-based refund per listing rules | Partial refunds + inventory release |
| Host — supply | Create/edit listing, photos, pricing, minimum nights | Search reindex on every material change |
| Host — calendar | Block dates, sync iCal, seasonal pricing | Concurrent edits vs in-flight bookings |
| Host — reservations | Accept/decline requests, pre-approve, co-host access | State machine + push within SLA |
| Host — payouts | Earnings, payout schedule, tax forms | Multi-currency, delayed settlement |
| Platform — trust | Identity verification, reviews both ways, fraud | Async ML + manual review queues |
| Platform — support | Disputes, rebooking, safety escalations | Timeline joins reservation + payments + messages |
Functional details worth stating clearly
Search results are not bookings. A card on the map is an indexed snapshot; only the booking service can claim dates.
Quotes expire. Nightly rates, fees, and tax lines are valid for a bounded TTL—refresh if the guest waits too long.
Request-to-book ≠ Instant Book. Dates may be soft-held until the host accepts; different timeouts and notification paths.
Out of scope today (say it aloud). Long-term leases, property management ERP, or building your own global card network—park them to keep the design focused.
Step 2 — Non-functional requirements (engineering promises)
| Category | Target (typical) | How we meet it | If we miss it |
|---|---|---|---|
| Latency — search | p95 < 300 ms for first page | Elasticsearch cluster, edge caching, bounded facets | Empty map; user bounces |
| Latency — listing detail | p95 < 200 ms above-the-fold | CDN photos, read replicas, denormalized cards | Slow conversion on mobile |
| Correctness — calendar | Zero double booking on same unit-night | Transactional hold + unique constraint | Legal and PR catastrophe |
| Consistency — money | Exactly-once charge per reservation | Idempotency keys, payment state machine | Double charge or lost revenue |
| Availability — booking API | 99.95%+ monthly | Active-active regions, graceful degradation | Global checkout outage |
| Freshness — search index | Seconds to minutes after host edit | Kafka → indexer consumers | Guest books “available” ghost nights |
| Scalability | Horizontal index shards + DB partitioning | Listing id sharding, ES index per region | Hot city melts one node |
| Privacy | Address reveal only after confirmed booking | Field-level ACL in API responses | Safety incidents |
Key idea: Discovery can be eventually consistent; inventory and money cannot. Put strict transactions on the booking loop only.
Step 3 — Napkin math (why one database is not enough)
Round numbers. Multiply in the open—you are showing magnitude, not audited financials.
- ~7–8 million active listings worldwide (order of magnitude cited in industry reporting).
- ~150M+ guest arrivals per year → ~400k reservation-night commits per day on average (peaks much higher on holidays).
- Search QPS: if 50M users touch search monthly and each session runs 5 queries, peak might reach tens of thousands of search requests per second globally—Elasticsearch clusters, not
SELECT * FROM listings. - Photos: 20 images × 2 MB each × millions of listings → object storage + CDN dominate bytes; metadata rows are tiny in comparison.
- Calendar writes: hosts block/unblock dates—lower QPS than search but each write must invalidate availability for upcoming guest searches.
Step 4 — Architecture: three loops
Guests and hosts on the left. Edge APIs behind a load balancer. Three cooperating backends: inventory (listings, calendars, pricing rules), discovery (search index, ranking, map aggregation), booking (quotes, holds, reservations, payments). Async buses (Kafka or similar) connect inventory changes to indexers and analytics.
flowchart TB
subgraph clients [Clients]
APP[Guest / host apps]
WEB[Web]
end
subgraph edge [Edge]
LB[Load balancer]
API[API gateway / BFF]
end
subgraph services [Core services]
INV[Inventory service]
SRCH[Search service]
BK[Booking service]
PAY[Payments service]
MSG[Messaging]
TR[Trust / reviews]
end
subgraph data [Data]
PG[(OLTP Postgres)]
ES[(Elasticsearch)]
R[("Redis cache")]
S3[(Photo storage)]
CDN[CDN]
end
APP --> LB --> API
WEB --> LB
API --> INV
API --> SRCH
API --> BK
BK --> PAY
API --> MSG
API --> TR
INV --> PG
BK --> PG
INV --> R
SRCH --> ES
INV --> S3
S3 --> CDN
INV -.->|listing.updated| SRCH
Step 5 — Walk one booking end to end
Guest “Sam” books a flat in Lisbon for Fri–Mon (3 nights), Instant Book enabled.
- Search —
GET /v2/search?location=…&check_in=&check_out=&guests=2hits Elasticsearch with geo filter + date availability bitset (or join on calendar service). - Listing detail — read path loads denormalized card from cache; photos from CDN; reviews from aggregate table.
- Quote — booking service computes nightly subtotal, cleaning fee, service fee, taxes; returns
quote_id+expires_at. - Hold — transactional insert into
calendar_holdsfor each night withlisting_id+dateunique constraint; statuspending_payment. - Pay — payments service authorizes card with idempotency key
Idempotency-Key: {quote_id}; on success emitspayment.captured. - Confirm — reservation moves to
confirmed; hold becomesbooked; search indexer consumes event and removes those nights from available inventory. - Notify — push/email to host; messaging thread opened; exact address unlocked for guest.
- Payout — after check-in (or policy window), payout job transfers host share minus platform fee.
stateDiagram-v2
[*] --> quoted
quoted --> pending_payment: guest checks out
pending_payment --> confirmed: payment OK
pending_payment --> expired: TTL
quoted --> pending_host: request to book
pending_host --> confirmed: host accepts
pending_host --> declined: host declines
confirmed --> cancelled_guest: guest cancel
confirmed --> cancelled_host: host cancel
confirmed --> completed: checkout
expired --> [*]
declined --> [*]
completed --> [*]
Step 6 — Discovery: search, geo, and ranking
Search is read-heavy and facet-rich: price range, room type, Instant Book, superhost, accessibility, cancellation policy.
Elasticsearch (or OpenSearch) is the usual engine: inverted indexes for text, geo_shape or geo_distance for map bounds, doc values for sorting.
- Availability in the index — denormalize “available for this check-in/check-out” as precomputed bitsets or nightly flags updated by consumers; avoid per-query joins to OLTP under load.
- Ranking — blend relevance, price, review score, booking probability, and business rules (promoted listings disclosed in UI).
- Map clustering — aggregate pins at low zoom; expand on zoom to keep payloads small.
Personalization (optional) — re-rank top 200 candidates with ML features; keep baseline search explainable for debugging.
Step 7 — Inventory: calendars, pricing, and double-booking prevention
Each listing has a calendar: one row per night (or bitmap per month) with status available | blocked | booked | hold.
The invariant: at most one confirmed booking per (listing_id, night).
-- Simplified night ledger CREATE TABLE calendar_nights ( listing_id BIGINT NOT NULL, night_date DATE NOT NULL, status TEXT NOT NULL, reservation_id BIGINT, version INT NOT NULL DEFAULT 1, PRIMARY KEY (listing_id, night_date) );
Hold pattern: insert rows with status=hold inside a transaction; on payment success flip to booked; on TTL expiry release to available.
Use SELECT … FOR UPDATE on the affected nights before insert to serialize races between two guests.
External calendars — iCal import runs async; treat as blocked with source tag; reconcile conflicts before confirm.
Sanity check: If double bookings appear, look for missing unique constraints or holds that never expire—not “Elasticsearch was slow.”
Step 8 — Pricing, fees, and quotes
Nightly price is only the start. A quote bundles:
- Base nights × rate (weekend/weekday rules, seasonal adjustments)
- Cleaning fee (per stay), extra guest fee, pet fee
- Platform service fee (guest side) and host fee (host side)
- Taxes (VAT, occupancy tax) computed by jurisdiction tables
- Currency conversion with FX rate locked at quote time
Store quote_snapshot_json on the reservation so disputes replay the exact line items shown at checkout.
Step 9 — Reservation state machine and timeouts
| State | Meaning | Typical trigger |
|---|---|---|
quoted | Price computed, no inventory held yet | User on checkout page |
pending_payment | Nights on hold; awaiting card | User tapped Pay |
pending_host | Request sent; host must respond | Non-instant listing |
confirmed | Money captured; nights booked | Payment OK or host accept |
cancelled_* | Policy-driven refund path | Guest/host/platform |
completed | Stay ended; review window open | Checkout date passed |
Timers: payment hold TTL (e.g. 15 minutes); host response SLA (e.g. 24 hours) with auto-decline and release holds. Cron or workflow engine (Temporal/Cadence-style) drives expirations—not sleeping threads in API pods.
Step 10 — Payments, refunds, and host payouts
Use a PSP (Stripe Connect–class pattern): platform account, connected host accounts, split charges.
- Authorize vs capture — authorize at booking; capture at check-in or immediately per product policy.
- Idempotency —
Idempotency-Keyon every charge/refund API call tied toreservation_id + action. - Refunds — partial refunds follow cancellation policy encoded as rules engine output, not ad-hoc support SQL.
- Payouts — batch transfers on schedule; reconcile PSP webhooks (
charge.succeeded) with internal ledger tables.
POST /v1/payments/charges
Idempotency-Key: res_9f2a_confirm
{
"reservation_id": "res_9f2a",
"amount_cents": 41200,
"currency": "EUR",
"guest_payment_method": "pm_…",
"application_fee_cents": 5200
}
Step 11 — Messaging, notifications, and photos
Messaging — thread per reservation; store messages in append-only log; deliver via WebSocket or push; scan for off-platform payment fraud.
Notifications — email/SMS/push via event bus (reservation.confirmed); template service; retry with dead-letter queue.
Photos — upload to object storage; async resize (WebP/JPEG variants); serve via CDN; never block listing publish on all sizes ready—show progressive enhancement.
Step 12 — Reviews, trust, and fraud
- Double-blind reviews — reveal after both submit or window closes to reduce retaliation bias.
- Aggregates — denormalize
avg_rating,review_counton listing doc for search sort. - Identity — document verification pipeline async; gate Instant Book on trust tier.
- Fraud — velocity checks on new accounts, stolen cards, fake listings; manual review queue fed by Kafka.
Step 13 — Growing the fleet: sharding and regional cells
- Shard OLTP by
listing_idorhost_idso hot hosts do not saturate one database. - Search — index per geography or alias with routing keys; reindex with blue/green index swap.
- Regional cells — EU users hit EU databases for PII; cross-region read replicas for global search catalog where law allows.
- Cache — Redis for hot listing cards and session carts; invalidate on
listing.updatedevents.
Step 14 — Technical layer: APIs and payloads
| Operation | HTTP | Success | Notes |
|---|---|---|---|
| Search stays | GET /v2/listings/search |
200 + results + pagination cursor |
Geo bbox, dates, filters; cache-safe only without personalized pricing |
| Get listing | GET /v2/listings/{id} |
200 |
Address fields redacted until guest has confirmed reservation |
| Create quote | POST /v2/reservations/quote |
200 + quote_id, expires_at |
Validates calendar still free |
| Confirm booking | POST /v2/reservations |
201 + reservation resource |
Body includes quote_id, payment method; idempotent on retry |
| Host respond | POST /v2/reservations/{id}/host_response |
200 |
accept or decline while pending_host |
Quote request (illustrative):
POST /v2/reservations/quote
Authorization: Bearer eyJ…
Content-Type: application/json
{
"listing_id": "lst_48k2",
"check_in": "2026-08-14",
"check_out": "2026-08-17",
"guests": { "adults": 2, "children": 0 },
"currency": "EUR"
}
Logical tables
listings(id, host_id, geo_point, title, …) calendar_nights(listing_id, night_date, status, reservation_id, version) reservations(id, listing_id, guest_id, state, quote_snapshot_json, …) payments(id, reservation_id, psp_charge_id, amount_cents, status) reviews(id, reservation_id, author_id, rating, body, …) search_documents(listing_id, indexed_at, availability_bitmap, …)
Step 15 — Reliability, observability, and failure modes
Failure modes
- Payment succeeded, booking not confirmed — reconciliation job matches PSP webhooks to reservation state; auto-refund or complete booking.
- Hold expired but card charged — never capture without confirmed nights; use auth-only until invariant holds.
- Stale search — show “prices may update” on checkout refresh; block confirm if quote ≠ live calendar.
- Indexer lag — degrade to stricter live calendar check at quote time even if search was optimistic.
Observability
- Trace search → quote → pay with
listing_id,reservation_id. - Metrics: search p95, quote error rate, hold conflict rate, payment webhook lag, indexer consumer lag.
- SLO example: 99.95% successful confirms among payment attempts; zero double-bookings per million nights.
Step 16 — Goals → knobs (quick reference)
| Goal | Knob |
|---|---|
| Search feels instant | Elasticsearch tuning, denormalized availability, CDN, regional replicas |
| No double booking | Night-level unique keys, transactional holds, FOR UPDATE |
| Revenue correct | Quote snapshots, idempotent payments, webhook reconciliation |
| Hosts trust payouts | Clear fee breakdown, PSP Connect, predictable payout dates |
| Safe marketplace | Verification tiers, messaging monitors, review integrity |
Step 17 — Close the loop (what to practice)
On a whiteboard: three loops, one booking story, label Postgres vs Elasticsearch vs Redis vs CDN on each step.
Out loud: five functional requirements per actor (guest, host, platform) and which NFR applies to discovery vs booking.
With the technical section: trace POST /v2/reservations/quote through hold, pay, and confirmed.
The one line to remember
Airbnb-class systems are three loops behind one search box: inventory (truth about nights), discovery (fast ranked search), and booking (quotes, holds, money, state). Keep calendar correctness in OLTP; let search be fast and slightly stale—but never looser than the quote step.