sharpbyte.dev

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:

Step 0 — How we will work through the problem

Ordered thinking beats memorizing boxes. Use this sequence when you design a lodging marketplace:

  1. Clarify scope. Nightly stays only, or Experiences too? Instant Book vs host approval? International payouts in scope?
  2. Write requirements. Functional = guest/host behaviors. Non-functional = search latency, calendar correctness, payment integrity.
  3. Do napkin math. Listings count, searches per second, bookings per day—so nobody assumes one MySQL row stores the planet.
  4. Draw three loops before naming Elasticsearch or Stripe.
  5. 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.

ActorRequirementWhy scale makes it hard
Guest — searchLocation, dates, guests, filters; map + list viewsGeo + facet queries over millions of listings
Guest — listingPhotos, amenities, house rules, reviews, host profileCDN media; aggregated review scores
Guest — bookPrice breakdown, taxes/fees, Instant Book or requestQuote must match calendar at commit time
Guest — tripItinerary, check-in instructions, messaging hostPost-booking async notifications
Guest — cancelPolicy-based refund per listing rulesPartial refunds + inventory release
Host — supplyCreate/edit listing, photos, pricing, minimum nightsSearch reindex on every material change
Host — calendarBlock dates, sync iCal, seasonal pricingConcurrent edits vs in-flight bookings
Host — reservationsAccept/decline requests, pre-approve, co-host accessState machine + push within SLA
Host — payoutsEarnings, payout schedule, tax formsMulti-currency, delayed settlement
Platform — trustIdentity verification, reviews both ways, fraudAsync ML + manual review queues
Platform — supportDisputes, rebooking, safety escalationsTimeline 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)

CategoryTarget (typical)How we meet itIf we miss it
Latency — searchp95 < 300 ms for first pageElasticsearch cluster, edge caching, bounded facetsEmpty map; user bounces
Latency — listing detailp95 < 200 ms above-the-foldCDN photos, read replicas, denormalized cardsSlow conversion on mobile
Correctness — calendarZero double booking on same unit-nightTransactional hold + unique constraintLegal and PR catastrophe
Consistency — moneyExactly-once charge per reservationIdempotency keys, payment state machineDouble charge or lost revenue
Availability — booking API99.95%+ monthlyActive-active regions, graceful degradationGlobal checkout outage
Freshness — search indexSeconds to minutes after host editKafka → indexer consumersGuest books “available” ghost nights
ScalabilityHorizontal index shards + DB partitioningListing id sharding, ES index per regionHot city melts one node
PrivacyAddress reveal only after confirmed bookingField-level ACL in API responsesSafety 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.

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.

  1. SearchGET /v2/search?location=…&check_in=&check_out=&guests=2 hits Elasticsearch with geo filter + date availability bitset (or join on calendar service).
  2. Listing detail — read path loads denormalized card from cache; photos from CDN; reviews from aggregate table.
  3. Quote — booking service computes nightly subtotal, cleaning fee, service fee, taxes; returns quote_id + expires_at.
  4. Hold — transactional insert into calendar_holds for each night with listing_id + date unique constraint; status pending_payment.
  5. Pay — payments service authorizes card with idempotency key Idempotency-Key: {quote_id}; on success emits payment.captured.
  6. Confirm — reservation moves to confirmed; hold becomes booked; search indexer consumes event and removes those nights from available inventory.
  7. Notify — push/email to host; messaging thread opened; exact address unlocked for guest.
  8. 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.

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:

Store quote_snapshot_json on the reservation so disputes replay the exact line items shown at checkout.

Step 9 — Reservation state machine and timeouts

StateMeaningTypical trigger
quotedPrice computed, no inventory held yetUser on checkout page
pending_paymentNights on hold; awaiting cardUser tapped Pay
pending_hostRequest sent; host must respondNon-instant listing
confirmedMoney captured; nights bookedPayment OK or host accept
cancelled_*Policy-driven refund pathGuest/host/platform
completedStay ended; review window openCheckout 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.

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

Step 13 — Growing the fleet: sharding and regional cells

Step 14 — Technical layer: APIs and payloads

OperationHTTPSuccessNotes
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

Observability

Step 16 — Goals → knobs (quick reference)

GoalKnob
Search feels instantElasticsearch tuning, denormalized availability, CDN, regional replicas
No double bookingNight-level unique keys, transactional holds, FOR UPDATE
Revenue correctQuote snapshots, idempotent payments, webhook reconciliation
Hosts trust payoutsClear fee breakdown, PSP Connect, predictable payout dates
Safe marketplaceVerification 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.