System design deep dive · HLD
Design Airbnb (listing search and booking marketplace): full high-level design.
Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for search and booking, and an entity-relationship diagram for the data model - with the reasoning an interviewer expects behind every box and arrow.
1. Clarify requirements before drawing any box
Airbnb's defining tension is that search must be fast and can tolerate slight staleness, while booking must be exactly correct and can never tolerate two guests being confirmed for the same listing on overlapping dates - those two halves need different consistency models.
Functional requirements
Non-functional requirements
2. Back-of-the-envelope capacity estimation
These numbers decide how the search index must be sharded geographically, how the availability calendar is stored so 7 million listings times 365 days doesn't become billions of rows nobody can query, and why bookings need a completely different consistency treatment than search.
| Metric | Assumption | Resulting estimate |
|---|---|---|
| Active listings | 7 million active listings globally | baseline for search index and calendar sizing |
| Search traffic | 150 million searches/day, 3x peak multiplier | ~1,740 searches/sec average, ~5,200/sec peak |
| Booking traffic | 2 million bookings/day, much smaller peak multiplier (bookings spread across a booking window, not bursty like search) | ~23 bookings/sec average, ~100/sec peak |
| Availability calendar rows | 7M listings × 365 days if stored per-day | ~2.5 billion day-rows - too many for a naive per-day table; favors a range-based representation instead (see ER section) |
| Search index size | ~2 KB/listing (geo, price, amenities, denormalized rating) | 7M × 2KB ≈ 14 GB - comfortably fits a sharded Elasticsearch cluster in memory-backed indices |
| Payout volume | 2M bookings/day × average $220/night × ~3.4 nights avg | ~$1.5B/day in gross booking value flowing through the payout ledger - underscores why this path must be exact, not eventually consistent |
3. High-level design (HLD)
The HLD names the major components and the two data flows - search and booking - without yet committing to sharding schemes, the specific consistency mechanism, or payout scheduling details.
What each box owns
Search service and search index
Handles the read-heavy, latency-sensitive path: geo bounding box, date range, and filter matching against a denormalized, eventually-consistent copy of listing and rough availability data. It never confirms a booking - it only shows candidates that were available as of the last index update, which may be seconds stale.
Booking service and availability calendar
The only component allowed to confirm a reservation. It re-checks the actual, current availability_calendar (the source of truth, not the search index) inside a single atomic transaction, and only proceeds to payment if that specific date range is genuinely free at that instant - this re-check is what makes the eventually-consistent search index safe to use for browsing.
Payment service
Charges the guest only after the availability calendar transaction has successfully claimed the date range, never before - reversing that order would risk charging a guest for a booking that then fails to confirm due to a race with another guest.
Payout ledger and the CDC pipeline
The payout ledger schedules and records the host's share of each booking as a durable, append-only, double-entry-style record, decoupled from the booking transaction itself so payout timing (e.g. 24 hours after guest check-in) doesn't hold up booking confirmation. A change-data-capture pipeline propagates calendar changes from the source-of-truth database into the search index asynchronously, which is exactly why the search index can be stale by design.
4. Detailed architecture diagram
The architecture diagram answers how geo search is actually sharded, how the availability calendar prevents double-booking at the database level, and how payouts stay correct under cancellations and refunds - the details an interviewer checks once the HLD shape is accepted.
| Decision | Choice | Reasoning |
|---|---|---|
| Search vs. booking consistency | Eventually-consistent search index, strongly-consistent booking database | Search must scale to thousands of QPS and can tolerate showing a listing that gets booked a moment later; the booking transaction is the sole gatekeeper that actually enforces no-double-booking. |
| Double-booking prevention | Postgres EXCLUDE constraint on (listing_id, daterange) rather than application-level locking | Pushes the correctness guarantee into the database itself, which atomically rejects an overlapping insert - safer than any distributed lock the application would have to manage correctly under concurrency. |
| Payout timing | Delayed until after the cancellation window, processed by a separate scheduler | Decouples "booking confirmed" from "money moved," so a late cancellation or dispute doesn't require clawing back funds already paid out. |
| Search index freshness | Asynchronous CDC with seconds of lag, not synchronous dual-write | Synchronously updating the search index on every booking would tie search infrastructure health to booking latency; a brief staleness window is an acceptable, well-understood trade-off given the booking service re-validates anyway. |
5. Sequence diagrams for the two critical flows
A sequence diagram is where an interviewer checks whether a candidate actually re-validates availability at booking time rather than trusting search results, and whether payment happens in the safe order relative to the calendar lock.
5.1 Geo + date-range search
The search response in step 4 is deliberately framed as a snapshot, not a promise - the index reflects the availability calendar as of whenever the CDC pipeline last synced, which could be a few seconds behind. This is why the booking flow below performs its own independent, authoritative check rather than trusting this result.
5.2 Booking a listing (atomic availability claim)
Step 2's database-level EXCLUDE constraint is the entire double-booking defense: if a second guest's request reaches step 2 for an overlapping date range before this transaction commits, that second insert is rejected by the database itself, with no need for the application to implement its own distributed lock. Step 4 charges payment only after step 3's commit succeeds - reserving the calendar before touching money means a failed or rejected booking never results in a stray charge.
6. Entity-relationship (ER) diagram and schema
The data model has to answer: how does the database itself prevent overlapping bookings without 2.5 billion per-day rows, how is a host's payout traceable back to a specific booking, and how does search stay independent of the booking system's write load.
Key modeling decisions
| Storage choice | Use when | Watch out for |
|---|---|---|
| Relational (Postgres) with range types for bookings/availability | You need an atomic, database-enforced guarantee against overlapping reservations - exactly the double-booking requirement. | Sharding by listing_id means a query spanning many listings (like search) is the wrong workload for this store - that's what the search index is for. |
| Document/search store (Elasticsearch) for the listings search index | Access pattern is geo + filter + rank across millions of listings at high QPS. | No transactional guarantees; must never be the system that confirms a booking, only the system that suggests candidates. |
7. Deep dives interviewers actually probe
How exactly do you guarantee zero double-bookings under concurrency?
The guarantee lives entirely in the database: a PostgreSQL EXCLUDE constraint using the btree_gist extension on (listing_id, date_range) makes any two overlapping ranges for the same listing mutually exclusive at the storage engine level. Two concurrent transactions both trying to insert overlapping bookings will have one succeed and one fail with a constraint violation - the losing request is told to retry against updated availability, rather than the application attempting to coordinate a distributed lock, which is slower and easier to get subtly wrong.
Why not just store one row per listing per day in the availability calendar?
At 7 million listings across a year, that's 2.5 billion rows for a mostly-available calendar where the interesting information is really just "which ranges are blocked." Storing only the blocked/booked ranges (via DATERANGE) instead of every individual day means the table's size tracks actual bookings and host blocks - tens of millions of rows - not the full space of listing-days, most of which are simply available and need no row at all.
How does the host payout ledger stay correct through cancellations and refunds?
Payout is deliberately not triggered at booking-confirmation time; a scheduler waits until the cancellation window has closed (and for stays already underway, sometimes waits until a set point after check-in) before writing a payout ledger entry. Each entry is idempotent, keyed by booking_id, so a scheduler retry or a duplicate trigger never double-pays a host, and a cancellation that happens before the scheduled payout simply removes the pending entry rather than requiring a reversal.
How does geo search stay fast across millions of listings and a flexible date range?
Listings are indexed with a geohash or S2-cell representation so a bounding-box query becomes a fast prefix/range lookup rather than a full haversine-distance scan. Date-range availability is pre-materialized into the search index as a compact per-listing bitset or blocked-ranges summary refreshed by the CDC pipeline, so filtering "available for these dates" is a cheap index-level check rather than a live join against the transactional booking database.
What happens if the CDC pipeline lags and search shows a listing that's actually just been booked?
This is an accepted, designed-for scenario, not a bug: the guest can attempt to book it, and the booking service's authoritative re-check (section 5.2, step 2) will reject the overlapping request against the real availability_calendar, returning a "no longer available" error so the guest can pick another listing. The alternative - making search synchronously consistent with every booking - would require search to share the booking database's write path, destroying the independent scalability that makes 5,200 searches/sec affordable in the first place.
Post a Comment
Add