At a glance#
| Number | Value | What it drives |
|---|---|---|
| Listings and hosts | 9M+ listings · 5.5M+ hosts (May 2026) | most homes are one unit: a night is free or taken |
| Nights booked | 533M in 2025 · ~1.46M a day · $91.3B | ~365,000 reservations a day at 4 nights a stay (assumed), ~13 / s at peak |
| Search | ~14,000 requests / s at peak (assumed) | search, not booking, sizes the read path |
| Listing updates | ~10,000 / s, mostly prices and availability | the index is updated by events, searchable within ~1 minute |
| Availability for every listing | 365 bits each · ~0.4 GB | filter by dates in memory; prices ~13 GB |
| Imported calendars | every 3 hours · ~1.5 h blind on average | ~4M feeds is only ~370 fetches / s; staleness, not load, is the risk |
| Money held | $6.9B at end 2025 · ~28 days of bookings | a ledger, not a payments table, is the system of record |
| Payout | business day after check-in · ~50 currencies | a payout scheduler with a holiday calendar per country |
Architecture#
DiagramSearch reads a copy; calendars, prices and money live elsewhere
%% title: Search reads a copy; calendars, prices and money live elsewhere
flowchart TD
G@{ icon: "sd:phone", label: "Guest app<br/>map rectangle · dates · guests" } ==> API@{ icon: "sd:api", label: "API gateway" }
API ==> ROOT["Search root<br/>fan out · merge · re-rank · pins"]:::hot
ROOT ==> LEAF["Search leaves<br/>geo cells · night bits · prices · ranker"]:::core
EXT@{ icon: "sd:globe", label: "Other travel sites<br/>.ics feeds" } -.-> ICAL@{ icon: "sd:cron", label: "iCal poller<br/>by risk · refresh at Reserve" }
ICAL --> CAL["Calendar service<br/>nights · blocks by source · rules"]:::service
PR@{ icon: "tech:spark", label: "Smart Pricing<br/>Spark batch + fast path" } --> CAL
CAL --> BUS@{ icon: "tech:kafka", label: "Kafka<br/>~10,000 listing updates / s" }
BUS ==> LEAF
API --> RES["Reservations<br/>unique listing + night"]:::service
RES --> RISK@{ icon: "sd:shield", label: "Risk scoring<br/>allow · friction · block" }
RES --> PAY["Payments<br/>pay-in · ledger · payouts"]:::core
class G client
class API edge
class EXT external
class BUS queue
class ICAL,PR,RISK service- Search reads, never asks: location, free nights, prices and rules live in the leaves' memory; a search never calls the calendar or pricing services.
- One strict write: a key on (listing, night) makes the second booking of a night fail; the risky double bookings are nights another site sold.
- Money runs on its own clock: charged at booking, held as a liability, paid out the business day after check-in.
DiagramThe ranking funnel, each stage an order of magnitude smaller
%% title: The ranking funnel, each stage an order of magnitude smaller
flowchart TD
F["Free for the dates and party<br/>~20,000 homes in a big search"]:::service
EBR["Retrieval · two towers, IVF clusters<br/>keep ~1,000"]:::core
DNN["First-stage ranker<br/>booking probability"]:::core
SET["Setwise re-ranker at the root<br/>the page as a whole"]:::hot
PINS["Map pins<br/>full pins · mini-pins"]:::ok
EMB@{ icon: "sd:ml", label: "Listing vectors<br/>rebuilt daily offline" }
F ==> EBR ==> DNN ==> SET ==> PINS
EMB -.-> EBR
class EMB storeKey flows#
DiagramRohan drags the map, then reserves Meera's home
%% title: Rohan drags the map, then reserves Meera's home sequenceDiagram autonumber participant R as Rohan's app participant S as Search root participant L as Leaves participant B as Reservations participant K as Risk scoring participant P as Payments R->>S: Map rectangle, 6–9 Nov, 5 guests S->>L: Fan out to every leaf L-->>S: Free all 3 nights, total price, ranked S-->>R: 18 cards, full pins and mini-pins R->>B: Reserve with the signed quote B->>K: Score this booking K-->>B: Low risk, no friction B->>B: Insert 3 nights, unique per listing-night B->>P: Charge ₹38,400, hold until check-in P-->>R: Confirmed
DiagramRefresh on intent: the Reserve button checks the other sites
%% title: Refresh on intent: the Reserve button checks the other sites
sequenceDiagram
autonumber
participant R as Rohan's app
participant B as Reservations
participant Q as iCal poller
participant O as Other site
participant C as Calendar service
R->>B: Reserve 6–9 Nov
B->>Q: Feeds older than a few minutes?
Q->>O: Conditional GET, about 1 s timeout
alt 304 not modified
O-->>Q: 304
Q-->>B: Nothing new
B->>C: Insert 3 nights
else New file blocks 7 Nov
O-->>Q: 200 with the new busy dates
Q->>C: Apply snapshot for this source
C-->>R: These dates are no longer available
end
Note over Q,O: no answer in time · book and flag the stay for the hostDiagramThe money, from Rohan's charge to Meera's payout
%% title: The money, from Rohan's charge to Meera's payout sequenceDiagram autonumber participant P as Pay-in participant L as Ledger participant S as Payout scheduler participant O as Payout participant M as Meera's bank P->>L: Charge posted · held for host, unearned fee, tax Note over L: every posting sums to zero, append-only S->>S: Check-in Friday 6 Nov · next business day Monday S->>O: Pay stay, key payout per stay O->>L: Intent recorded before the call O->>M: Send rupees with the same key M-->>O: Accepted O->>L: Payable out, payouts in transit
Data model#
DiagramCore entities
%% title: Core entities
classDiagram
class Listing {
+listingId : int
+geoCell : cell id
+freeNights : 365 bits
+nightlyPrice : 365 values
+minNights : int
+checkinDays : weekdays
}
class StayNight {
+listingId : int
+night : date · unique with listing
+stayId : string
}
class CalendarBlock {
+listingId : int
+night : date
+source : feed id or host
+uid : iCal event id
}
class Quote {
+listingId : int
+nights : date range
+total : money
+expires : minutes
}
class Posting {
+key : idempotency key
+currency : string
+entries : balanced, sum zero
}
class Payout {
+key : payout per stay
+host : hostId
+status : INTENT / SENT / FAILED
}
Listing "1" --> "many" StayNight : booked nights
Listing "1" --> "many" CalendarBlock : imported or manual
Quote "1" --> "0..1" StayNight : becomes
StayNight "many" --> "many" Posting : money events
Posting "many" --> "1" Payout : paid by- Search leaves hold Listing for all 9M+ listings: ~0.4 GB of night bits and ~13 GB of prices, partitioned by listing id so a festival never makes one shard hot.
- CalendarBlock rows are replaced only by the next good snapshot of the same source; they never erase a StayNight or a host's own block.
Lifecycle#
DiagramA stay, from Reserve to payout
%% title: A stay, from Reserve to payout stateDiagram-v2 [*] --> QUOTED: search returns a signed quote QUOTED --> VERIFYING: risk score in the middle QUOTED --> PAYMENT_PENDING: low risk VERIFYING --> PAYMENT_PENDING: friction cleared VERIFYING --> REJECTED: friction failed or blocked PAYMENT_PENDING --> CONFIRMED: charge or webhook succeeds CONFIRMED --> CHECKED_IN: check-in day · fee becomes revenue CHECKED_IN --> PAID_OUT: business day after check-in CONFIRMED --> CANCELLED: refund by reversing entries PAID_OUT --> [*] REJECTED --> [*] CANCELLED --> [*] class PAID_OUT ok class REJECTED bad class VERIFYING queue class PAYMENT_PENDING queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Availability in search | Night bits and prices in leaf memory, updated by events | Calling the calendar service per candidate | thousands of candidates per search at ~14,000 searches / s |
| Leaf partitioning | By listing id, fan out to all leaves | By region | a festival in Goa would melt a regional shard |
| Retrieval index | Two-tower vectors, IVF clusters as a filter field | HNSW graph | HNSW memory grew under ~10,000 updates / s and did poorly with filters |
| Map pins | Fewer, likelier pins · mini-pins on desktop | All 18 as full pins | 95% of searchers click ≤ 12 pins; mini-pins get ~8× fewer clicks |
| Own double bookings | Unique key on (listing, night) | Counts and locks | most homes are one unit per night |
| iCal staleness | Blocks tagged by source, polling by risk, refresh at Reserve | A fixed 3-hour round-robin | ~1.5 h average blind window; the Reserve press is when it bites |
| Price changes | Host range, ±15% a step, open quotes untouched | Applying the model's price directly | hosts must trust it; guests pay the quote they saw |
| Money | Append-only double-entry ledger, payout key before the call | Status columns on a payments table | billions held for weeks; retries must never pay twice |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Another site books a night between polls | two guests for one house | refresh on Reserve with a short timeout; poll risky feeds every few minutes |
| A partner serves an empty or shifted .ics | months of taken nights shown free | keep the last good snapshot; confirm big drops twice; alert the host |
| Price rises during checkout | a surprise total | signed quote with an expiry; pricing skips open quotes |
| Index lag grows to 15 minutes | stale prices, more "just booked" | alert on event-to-searchable; reserve-time check stays exact |
| Payout day is a bank holiday or the account is closed | late or returned payout | holiday calendar per country; reversing entries back to payable |
| Fraud ring on New Year's Eve | chargebacks weeks later | targeted friction for last-minute new accounts on hot nights |
| Risk model times out | slow or failed checkout | conservative fallback rules inside the latency budget |
Say it in the interview#
- "Search reads a copy, Reserve reads the truth." Night bits and prices in leaf memory for speed; a unique key on (listing, night) for exactness.
- Where, then when, then rank: geo cells, a bit mask for the nights, stay rules, then retrieval, a deep ranker and a setwise re-ranker.
- Maps aren't lists: attention is spread over pins, so show fewer, likelier pins, and mini-pins for the rest on desktop.
- Polling's cost is the blind window, not the load: ~4M feeds at 3 hours is ~370 fetches a second; tag blocks by source and refresh at Reserve.
- Pricing proposes, guardrails decide: host range, step limits and signed quotes keep prices trustworthy.
- The ledger is the product: every posting balances, nothing is edited, and every payout has a key written before the call.
- Trust is friction plus honesty: risk scores trigger micro-authorisations or 3-D Secure; reviews stay sealed until both are in or 14 days pass.