Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

System Design

Design Airbnb

Step 1 / 9

Learn system design by building a lodging marketplace like Airbnb step by step.

The numbers to beatquadtreespatial splitviewportbounded querymsgeo lookup

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is Airbnb?

A two-sided marketplace: hosts list homes, guests search by place and dates and book a stay. The reads (search) are enormous and demand geo + availability filtering; the writes (bookings) are rare but absolutely must never double-book a home.

Guestsearch · book
New in this step: Guest.

Build a fast, filterable search over a spatial index and availability calendars, then a transactional booking path that reserves dates atomically and holds payment. Search optimizes for scale; booking optimizes for correctness.

What the new pieces do

Guestclient
Searches by place and dates, browses listings, and books a stay. Search is the heavy, read-mostly path; booking is rare but must be rock-solid.

Step 1 · The skeleton

List and find homes

Hosts need to publish listings and guests need to find them. Even ignoring geography and dates, you need a clean split between the catalog of homes and the act of searching it.

GuestHostAPI GatewaySearch ServiceListings DB
New in this step: Host, API Gateway, Search Service, Listings DB. · swipe to pan the diagram

Guest searches massively outnumber host writes. How do you structure search vs the catalog?

  1. Heavy search load hammering the source-of-truth DB threatens the writes (new listings, bookings) and can’t scale independently. Search should not contend with the system of record.

  2. The gateway routes host writes to the Listings DB and guest queries to a search service reading an index derived from it. Search scales for read traffic while writes stay simple — and a search overload never threatens the source of truth.

  3. You can’t pre-cache the combinatorial explosion of place × dates × filters, and listings/availability change constantly. You need a live searchable index, with caching layered on later for hot queries.

An API Gateway sends host writes to a Listings DB and guest queries to a Search Service that reads from it. This separation lets search scale for heavy read traffic while writes stay simple and consistent.

Why this piece earns its place

Splitting the paths is right, but notice what it concentrates: the same API Gateway now fronts a flood of browsing and the rare booking write. The moment search spikes on a popular weekend, those requests occupy every connection, worker and downstream socket the gateway has, and a booking queues behind people who are only looking. You carefully separated the data and then reunited the load at the front door. So I would give the two paths their own budgets there — separate connection pools and concurrency limits per route, with the booking route holding a small reserved share it never lends out — and shed search load first when the gateway saturates, because a guest who cannot browse retries and a guest who cannot pay leaves. The same logic sets timeouts: search should fail fast and let the guest try again, while a booking midway through committing must not be cut off by an impatient gateway. Isolation is not only about which database a request touches; it is about which queue it waits in.

What the new pieces do

Hostclient
Publishes a listing with photos, price and an availability calendar. Writes are infrequent compared to the flood of guest searches.
API Gatewaybackend
The single entry point routing searches to the search service and bookings to the booking service, behind a load balancer.
Search Serviceservice
Finds listings matching a location, date range, price and amenities, ranked for relevance. The most-hit service in the whole system.
Listings DBstore
The source of truth for listings: descriptions, photos (in blob storage), pricing rules and host info. Read constantly during search.

Step 2 · Search by place

Geo indexing

“Homes near this map area” is the core query, but computing distance to every listing on earth per search is impossibly slow. A normal database index on lat/long doesn’t handle 2D proximity well.

Search Servicegeo + filtersGeo Indexquadtree / geohash
New in this step: Geo Index.

"Homes near this map area" is the core query. How do you avoid computing distance to every listing on earth?

  1. A 1-D index can’t answer 2-D proximity — nearby in one axis can be far in the other, so you’d still scan and distance-check huge ranges. 2-D search needs a spatial structure.

  2. Distance from one origin doesn’t help search from arbitrary map locations and must be recomputed per query. You need a structure indexed by region, not by distance to one point.

  3. Geohashing turns 2-D coordinates into a sortable prefix; quadtrees split dense areas. Proximity search becomes a bounded lookup of listings in the viewport, then filter/rank that small set.

Build a spatial index — a quadtree or geohash — that buckets listings by region so you can fetch just the ones inside the viewport quickly, then filter and rank that small set.

Why this piece earns its place

The spatial index hands you one knob — how finely you cut space — and it is not free in either direction. Geohash prefix length, or the point at which a quadtree splits a cell, decides how many listings come back before any filtering happens. Cut too coarsely and a dense city lands in a single cell, so the viewport lookup returns nearly every home in that city and you do the planet-scan work one level down, in your own process. Cut too finely and an ordinary map view straddles many cells, so one query becomes many lookups plus a merge, and the index itself grows. I would tune it to a target candidate count per query rather than to a tidy number of digits, which is exactly why a quadtree is attractive — it splits only where density demands it, so a packed city centre and an empty county get different depths from the same rule. Either structure also needs the boundary case handled: a home just over a cell edge can be nearer than one inside, so a query reads the neighbouring cells too and ranks on real distance. Cell membership is a shortlist, never the answer.

  • quadtreespatial split
  • viewportbounded query
  • msgeo lookup

What the new pieces do

Geo Indexindex
A spatial index that answers “listings within this map area” quickly, instead of scanning every home on earth and computing distances.

Back of the envelope

geohash = sortable prefix
2-D coords → a 1-D bounded range
quadtree splits dense areas
a viewport query, not a planet scan
fetch in-viewport, then filter/rank
work on a small candidate set

Step 3 · Only what’s free

Availability calendars

Guests search by date range, so results must exclude homes already booked for those nights. Availability changes constantly as bookings happen, and it gates both search and booking.

Search Servicegeo + filtersGeo Indexquadtree / geohashListings DBhomes · pricesAvailabilitycalendar / listing
New in this step: Availability.

Guests search by date range, so results must exclude homes booked for those nights. How do you model "is it free?"

  1. A home is free some nights and taken others — a single flag can’t express a calendar. A stay spans a contiguous range of nights that must all be free.

  2. One authoritative availability calendar per listing lets search filter by the requested range and booking later reserve against the same calendar. Modeling a date range makes both filtering and the no-double-book check natural.

  3. Scanning and aggregating all bookings per listing on every search is far too slow on the hottest path. Maintain availability as its own authoritative, queryable calendar updated as bookings happen.

Keep a per-listing Availability calendar of open/taken nights. Search filters candidates by the requested range; booking will later reserve against the same calendar. One authoritative place for “is this home free?”.

Why this piece earns its place

This calendar quietly becomes the most contended object in the system, so the assumption worth making explicit is who is allowed to write it. It is tempting to treat it as a projection of the bookings table, but it is not: hosts block dates by hand, they import calendars from other listing sites, turnover and cleaning buffers close the night after a checkout, and minimum-stay rules make some open nights unbookable anyway. Each of those is a separate writer, and any one that edits the calendar outside the guarded path the next step builds can hand back a night that is already sold. So every mutation — host block, external import, admin fix — goes through the same reserve-and-release operation, with booking as just one caller of it. The second thing I would pin down is the horizon: a night-granular calendar per listing grows without limit if you let it, so bookings open only a bounded window into the future and past nights are archived out. And free is not one state. A night held by an in-flight booking is neither open nor sold, and search has to treat a held night as taken or it advertises inventory that is already spoken for.

What the new pieces do

Availabilitystore
Per-listing calendar of which nights are open or taken. Both search (filter by dates) and booking (reserve dates) depend on it being correct.

Step 4 · Never double-book

Transactional booking

Two guests try to book the same home for overlapping dates at the same instant. Without care, both succeed — and one shows up to a taken house. This is the marketplace’s cardinal sin.

API GatewayListings DBBooking ServiceAvailability
New in this step: Booking Service. · swipe to pan the diagram

Two guests try to book the same home for overlapping dates at the same instant. How do you guarantee only one wins?

  1. A gap between check and write is the classic race — both guests see "free", both write, both think they booked. Check-then-set must be one indivisible step.

  2. Confirming two overlapping stays means someone arrives at a taken house — the marketplace’s cardinal sin. The conflict must be prevented at write time, not reconciled after.

  3. Check the range is free and mark it taken together, guarded by a unique (listing, night) constraint or row locks. Concurrent conflicting bookings collapse to exactly one winner; the other cleanly retries.

The Booking Service reserves the requested nights in a single atomic transaction: check the range is free and mark it taken together, guarded by a constraint or lock on the listing+dates. Concurrent conflicting bookings — exactly one wins.

Why this piece earns its place

The thing to say out loud is where this transaction ends, because the next step adds a network call to a payment provider and the instinct is to put it inside. Do not. Holding locks on a listing’s nights while waiting on a third party means one slow response freezes that home for everyone, and a provider outage freezes it until the locks time out. So the transaction commits quickly and marks the nights held against this booking; the money happens after, and a held range that never confirms is released. The nights are still exclusive — the constraint does that — they are simply not yet paid for. Two smaller details matter under real concurrency. Lock or insert the nights of a range in a consistent order, or two overlapping bookings grabbing the same rows from opposite ends deadlock instead of one of them cleanly losing. And treat the loser as a normal outcome rather than an error: the second guest should get a specific taken-dates response that re-runs their search with those nights excluded, because a generic failure plus a client retry loop turns one lost race into a stampede against the hottest listing on the site.

  • atomicreserve
  • 1winner per night
  • 0double-bookings

What the new pieces do

Booking Serviceservice
Turns “I want these dates” into a confirmed reservation — atomically, so two guests can’t book the same home for overlapping nights.

Back of the envelope

check + mark taken = 1 transaction
no gap for a race
unique (listing, night)
the DB enforces one winner
loser retries cleanly
0 double-bookings

Step 5 · Take the money

Payment hold & capture

Confirming a stay involves money across two parties, and payment can fail or be retried. Charging twice, or reserving dates without securing payment, both break trust.

Booking Servicereserve datesPaymentshold + capture
New in this step: Payments.

Confirming a stay involves money and the payment can fail or be retried. How do you take it safely?

  1. Reserving without securing funds means confirmed dates with no guaranteed payment, and no clean way to release them on failure. Tie the hold to the booking and release both if payment fails.

  2. Hold the card to secure funds while finalizing, capture on confirmation, and release the hold and the dates if it ultimately fails. An idempotency key makes the inherently retried payment path safe from double-charges.

  3. Charging before a booking is even confirmed means refunds, double-charges on retries, and money taken for stays that never happen. Separate auth from capture.

On booking, authorize (hold) the guest’s card; on confirmation, capture it and schedule the host payout. Use an idempotency key so a retried request never double-charges, and release the hold (and the dates) if payment ultimately fails.

Why this piece earns its place

The hard part of hold-and-capture is not the happy path, it is not knowing what happened. A request to the provider can time out after the authorization was created, so the safe assumption is that any ambiguous response might have moved money. The idempotency key makes a retry safe, but it does not tell you the answer — you still have to go and ask. So pending bookings need an owner: a reconciliation worker that takes every booking stuck in pending, queries the provider by that same key, and drives it to confirmed or released. Without one, an ambiguous timeout leaves nights held indefinitely for a stay nobody will take, and the inventory is the real hostage here, not the money. That also means the release path needs a deadline rather than a human — if the outcome is still unknown after a bounded wait, release the dates so the home can be sold and reverse the hold if it turns out to exist. And that deadline is not mine to set freely, because the hold is short-lived: an authorization is only good for a bounded window on the issuer’s side, so the wait has to end inside it. A booking still pending when the hold lapses can no longer be captured, so the worker treats a lapsed authorization as it treats a declined one — re-authorize if the guest is still there, otherwise release the nights.

What the new pieces do

Paymentsservice
Authorizes the guest’s card to hold funds at booking and captures on confirmation, with idempotency so a retry never double-charges.

Step 6 · Keep it fresh

Reviews & reindexing

New listings, price changes and reviews must show up in search, and ranking should reflect quality — but doing that work synchronously on the booking/listing path would slow everything down.

API GatewaySearch ServiceGeo IndexListings DBBooking ServiceAvailabilityPaymentsEvents
New in this step: Events. · swipe to pan the diagram

New listings, price changes and reviews must reach search, and ranking should reflect quality. How, without slowing writes?

  1. Doing reindexing and review aggregation inline makes every write wait on search-index updates, coupling a fast critical path to slow propagation. These effects are eventually-consistent by nature.

  2. Querying the source-of-truth DB per search reintroduces the read load on writes you separated in step 1, and can’t do fast ranking. Search needs its own index, kept fresh out-of-band.

  3. Listing/booking changes flow to an event stream; consumers reindex search, roll reviews into ratings, update payouts and notify — off the request path. Writes stay fast, search stays current (eventually).

Emit listing and booking changes as events. Consumers reindex the search service, aggregate reviews into ratings, update host payouts and send notifications — all asynchronously, so the write path stays fast and search stays current.

Why this piece earns its place

An event stream is only as good as its delivery guarantees, and the realistic ones are at-least-once and not necessarily in order. So every consumer here has to be idempotent and version-aware: carry the source row’s version on the event and drop anything older than what the index already holds, or a redelivered price change will cheerfully overwrite the newer price that followed it. Reprocessing the same event twice should be a no-op, because it will happen. The second thing is that these consumers are not equals and should not share a lane. Availability and booking changes are the most perishable things on the stream — they are what keeps search from showing sold homes as free — while review aggregation and payout rollups are slow, batchy and unhurt by a delay. Put them on separate streams or partitions so a backlog in the slow work cannot head-of-line block the urgent work, which is exactly what would otherwise happen at peak, when both are heaviest. And the signal I would alert on is consumer lag, not consumer errors: a healthy consumer falling steadily behind is the failure that reaches guests, and it throws no exceptions at all.

What the new pieces do

Eventsbus
A stream of listing and booking changes that drives search reindexing, review aggregation, host payouts and notifications asynchronously.

Back of the envelope

change ⇒ event
the write path doesn’t wait
consumers: reindex, reviews, payouts
eventually consistent by nature
record change ≠ propagate effects
decoupled via the stream

Step 7 · Scale the reads

The sharp edges

Search traffic dwarfs everything and spikes seasonally and by destination; popular cities create hotspots. One listings database can’t serve it all, and recomputing identical searches is wasteful.

GuestHostAPI GatewaySearch ServiceGeo IndexListings DBBooking ServiceAvailabilityPaymentsEvents
The system as it stands at this step. · swipe to pan the diagram

Add read replicas for the listings data, cache popular search results and listing pages, and shard/partition by region. Pricing and ranking can be precomputed; the rare booking path stays transactional while reads scale out horizontally.

Why this piece earns its place

“Cache popular searches” is easy to say, and the hard part is admitting what the key is. A search is a place, a date range and a set of filters, and that product is enormous — almost no two guests ask the identical question — so caching whole result sets buys a hit rate near zero, while every booking invalidates a set of entries you cannot cheaply enumerate. What actually repeats is underneath it: the listing pages themselves, the candidate set for a given map cell, photos and static content at the edge, and precomputed ranking signals like rating and popularity. Cache those, and let the per-query work be filtering and ranking over a small set. The replicas need one carve-out too. Reading availability from a replica is fine while browsing, since search was already best-effort — but the moment a guest has booked, their confirmation and trips page must read the primary, because showing someone their own reservation missing is a support ticket and, worse, a second booking attempt. Replicas serve the reads that tolerate being behind; anything a user just caused reads from where they wrote it.

You did it

You just designed Airbnb.

GuestHostAPI GatewaySearch ServiceGeo IndexListings DBBooking ServiceAvailabilityPaymentsEvents
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • A gateway splitting heavy search reads from rare, careful booking writes.
  • A spatial index (quadtree/geohash) makes “homes near here” a bounded query.
  • Per-listing availability calendars power both date filtering and booking.
  • Atomic, lock-guarded booking so overlapping stays can never double-book.
  • Payment hold-then-capture with idempotency keys for safe, retryable charges.
  • Event-driven reindexing and review aggregation keep search fresh asynchronously.
  • Read replicas, search caching and regional sharding scale the read-heavy load.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. How do you prevent double-booking under high concurrency?

    Make the reserve a single atomic operation: a unique constraint on (listing, night) so the second insert fails, or a row lock / SELECT … FOR UPDATE on the calendar range inside a transaction. Optimistic concurrency (version the calendar, retry on conflict) also works. The invariant is check-and-set as one indivisible step — never check, think, then write.

  2. Search shows a listing as available but it gets booked first — what happens?

    Expected: search is eventually-consistent and best-effort; the booking transaction is the authority. The guest picks dates that looked free, and the atomic reserve is the real check — if someone won the race, the booking fails and the guest picks again. You never rely on search availability being perfectly live.

  3. How do you keep the search index fresh as listings and bookings change?

    Emit change events from the write path; a consumer updates the search, geo and review indexes asynchronously. There’s a small lag, fine for discovery. You can push booking events at higher priority so date filters don’t go too stale, but the booking transaction still backstops correctness.

  4. How would you add surge / dynamic pricing?

    Compute it off the hot path: a pricing service consumes demand signals (searches, booking velocity, events, seasonality per region) and precomputes/caches a price per listing/date that search and the listing page read. Like ad-pricing or ride-hailing surge, it’s an async consumer feeding the read path, not a synchronous step in the booking transaction.

  5. Why optimize search and booking so differently?

    Opposite needs. Search is enormous, read-heavy, latency-sensitive and tolerant of slight staleness — scale it with replicas, caches, derived indexes, regional sharding. Booking is rare and must be exactly right — keep it transactional on the primary. Co-designing them would force one to compromise; separating lets each be optimal.

Check yourself — the answers, and why

Eight steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Search runs against a derived index (not the write DB) so that…

    Separating search from the system of record lets reads scale without endangering writes.

  2. "Homes near here" is fast because of…

    Spatial indexing makes proximity a bounded viewport lookup, not a planet-wide scan.

  3. Double-booking is prevented by…

    Check-and-set must be indivisible — the DB enforces exactly one winner per night.

  4. Payment uses authorize-then-capture so that…

    Hold at booking, capture on confirm; an idempotency key makes the retried path safe.

  5. Reindexing and review aggregation run asynchronously because they’re…

    Emit change events; consumers propagate effects so writes stay fast and search stays current.

Worked example: one Paris search, and two guests racing for the same flat

Guests A and B both search Paris for Jun 3–7. That is 4 nights (the 3rd, 4th, 5th and 6th), checking out on the 7th. Both book flat L in the same second. The candidate counts are illustrative.

StepWhereWhat happensNumbers
1API GatewaySends both searches to the search route's own connection pool. If search saturates, it is shed first, and the booking route keeps a reserved share it never lends out.2 searches, separate budgets
2Search Service + Geo IndexThe viewport maps to geohash cells, plus the neighbouring cells for homes just over an edge. Those cells return a shortlist, ranked later on real distance.~1,200 candidates (illustrative)
3AvailabilityKeeps only listings with all 4 nights open. A night held by an in-flight booking counts as taken.~300 left (illustrative)
4Search ServiceRanks the survivors using precomputed signals. L shows as free to both guests, because the index is a copy, best-effort by design.same answer, 2 guests
5Booking Service → AvailabilityEach booking inserts (L, Jun 3) to (L, Jun 6) in date order, in one short transaction under a unique (listing, night) constraint. A commits the nights as held. B hits the constraint and gets a taken-dates reply.4 rows, 1 winner, 0 deadlocks
6PaymentsAfter commit, authorizes a hold on A's card with an idempotency key. If the provider times out, a reconciliation worker queries it by that key, well inside the authorization's validity window.1 hold, 0 double charges
7Payments (confirm)Captures on confirmation, and the 4 nights go from held to sold. Had the payment failed, the hold and the nights would both be released.1 capture, host payout scheduled
8Events → SearchThe booking event goes on the urgent stream so search stops showing L for those nights. Review and payout work runs in a slower lane. A's trips page reads the primary, not a replica.alert on consumer lag

Rows 1 to 4 are allowed to be slightly wrong, and row 4 is: both guests were shown a flat only one could get. Correctness comes entirely from row 5, the unique constraint in Step 4. Keeping payment outside that transaction (Step 5) stops a slow provider from freezing L for everyone. At 10× traffic, search breaks first, not booking. A dense city crowds into one geo cell, so candidate sets explode. Step 2 adapts cell depth to density, and Step 7 caches the parts that repeat behind read replicas.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • List a home: a host publishes a listing with photos, price and an availability calendar.
  • Search: guests query by place, date range, price and amenities, ranked for relevance — the hot path.
  • Availability: results exclude homes already taken for the requested nights.
  • Book: reserve a contiguous range of nights atomically — never double-book a home.
  • Pay & review: hold-and-capture the guest’s card on confirmation; reviews feed ranking.

The qualities that shape everything

Each one names the mechanism that buys it.

Heavy search never threatens the source of truth
Search reads a derived index while host writes go to the Listings DB — so a search overload or a bad reindex never endangers the booking-critical data.
“Homes near here” without a planet-wide scan
A spatial index (quadtree / geohash) buckets listings by region, turning proximity into a bounded viewport lookup over a small candidate set.
Filter by date range
A per-listing availability calendar of open/taken nights that both search filtering and the booking reserve read from.
Never double-book a home
The booking service reserves the nights in one atomic transaction, guarded by a unique (listing, night) constraint or row lock — concurrent conflicts collapse to a single winner.
Safe, retryable payments
Authorize (hold) at booking and capture on confirm, with an idempotency key so a retried charge never double-charges and the dates release if payment fails.
Search stays fresh without slowing writes
Listing and booking changes emit events; consumers reindex search and aggregate reviews asynchronously, off the write path.
Absorb read-heavy, spiky traffic
Read replicas for listings, cached popular searches and listing pages, and regional sharding — while the rare booking path stays transactional on the primary.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

A derived search index over searching the write database directly

Running heavy search straight against the source-of-truth DB makes reads contend with listings and bookings and can’t scale on its own. A separate index lets search scale, and a search overload never threatens the writes.

A spatial index over a lat/long B-tree

A 1-D index can’t answer 2-D proximity — nearby on one axis can be far on the other, so you still scan huge ranges. A quadtree/geohash makes “near here” a bounded region lookup.

One atomic reserve over check availability then write

A gap between check and write is the classic race: both guests see “free”, both write, someone arrives at a taken house. A unique (listing, night) constraint makes check-and-set indivisible.

Authorize-then-capture over charging immediately

Charging before the booking is confirmed means refunds, double-charges on retries and money taken for stays that never happen. Hold-then-capture unwinds cleanly, and an idempotency key makes the retried path safe.

Async reindex via events over synchronous reindex on the write

Reindexing and review aggregation inline make every write wait on slow index updates. These effects are eventually-consistent by nature, so an event stream keeps writes fast and search current.

The answer, out loud

What a strong answer to “Design Airbnb” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–4 min

    What makes this hard

    Let me start with what makes this hard, because it isn’t the CRUD. Airbnb is two-sided: hosts publish homes, guests search and book. Those two flows have nearly opposite requirements. Search is enormous, read-heavy, and honestly tolerant of being a little wrong — if a home in the results turns out to be gone, that’s survivable. Booking is rare, and it has to be exactly right, because the one unforgivable outcome in a lodging marketplace is two guests arriving at the same house on the same night. So I’m going to design them as two systems that share data, and optimise search for scale and booking for correctness, rather than hunting for one design that’s mediocre at both.

  2. 4–9 min

    The skeleton

    The skeleton is a gateway in front of two things. Host writes go to a listings database, the source of truth for homes, prices and photos. Guest queries go to a search service reading an index derived from that data, not the database itself. I wouldn’t run search straight against the write store, because search is the biggest load in the system and I don’t want it competing with the writes bookings depend on. The cost is that search is a copy, so it’s always slightly behind, and I’ll make the booking path the authority instead of pretending otherwise.

    Built in step 1: List and find homes
  3. 9–15 min

    Homes near here

    Then the core query: homes near this map area. A normal index on latitude and longitude doesn’t answer that, because being close on one axis says nothing about the other. What works is indexing space itself — a geohash, which turns a coordinate into a sortable prefix so a region becomes a range, or a quadtree, which keeps splitting a cell until it holds few enough listings. Either way, “near here” stops being a scan and becomes “fetch the listings in this viewport”. That hands me a small candidate set, and everything expensive after it — filters, ranking — runs over that set rather than over the world.

    Built in step 2: Geo indexing
  4. 15–21 min

    Dates, not just homes

    Next, dates, because guests don’t search for a home, they search for a home on specific nights. I’d keep a per-listing calendar of which nights are open and which are taken. A single booked flag can’t express this — a home is free some nights and not others . One calendar per listing gives search something cheap to filter on and gives booking something to reserve against. I like that both sides read the same object: the thing the guest was shown and the thing the booking checks aren’t two different notions of free.

    Built in step 3: Availability calendars
  5. 21–28 min

    The race I actually care about

    Now the part I actually care about. Two guests hit book on the same home for overlapping nights at the same instant. If I check availability and then write the booking, there’s a gap, and both see free and both write. So the reserve has to be one indivisible operation: check the range is free and mark it taken in a single transaction, with the database enforcing it — a unique constraint on listing and night, or locks on those calendar rows. Concurrency then stops being my problem and becomes the database’s: exactly one booking wins each night, and the other fails cleanly and retries. I’d much rather have a visible, correct failure for one guest than a silent double-booking for both.

    Built in step 4: Transactional booking
  6. 28–34 min

    Taking the money

    Payment sits right after that. I’d authorize the card when the booking is made, which holds funds without taking them, and capture once the booking is confirmed, then schedule the host payout. Charging immediately is worse than it sounds: you’re taking money for a stay that isn’t confirmed yet, and every retry risks a second charge. With a hold, if anything fails I release the hold and release the nights, and nothing was taken. And because payment requests get retried by their nature, each one carries an idempotency key, so the provider treats a repeat as the same operation rather than a new charge.

    Built in step 5: Payment hold & capture
  7. 34–40 min

    Keeping search current

    That’s the booking path, so the remaining question is how everything stays current. New listings, price edits and reviews all need to reach search, and ranking should reflect quality. I wouldn’t do any of that inside the write. The write emits an event, and consumers off that stream reindex search, roll reviews into ratings, update payouts and send notifications. The write path stays fast, and the propagation is eventually consistent. Discovery can sit a little behind reality; the booking transaction is what stops that from ever turning into a wrong outcome.

    Built in step 6: Reviews & reindexing
  8. 40–45 min

    Scaling, and the trade-off

    Finally, scale, which here mostly means reads. Read replicas for the listings data, caching for the things that genuinely repeat — listing pages, static content, precomputed ranking signals — and partitioning by region, which works because searches are already bounded to a viewport. Booking stays transactional on the primary, because it’s rare and it’s the one thing that must be right. That’s the trade-off I’d leave on the table: I’m deliberately letting search be stale and cheap so booking can be strict and expensive. With more time I’d take pricing off the hot path — a service consuming demand signals and precomputing a price per listing and date — and work through cancellations and refunds, which run booking and payment backwards and are where the real edge cases live.

    Built in step 7: The sharp edges

What this teaches

Learn system design by building a lodging marketplace like Airbnb step by step. An interactive guide covering listings, geo search with filters, availability calendars, booking without double-booking, the payment hold-and-capture flow, reviews and reindexing, and scaling reads.

Key takeaways

  • A gateway splitting heavy search reads from rare, careful booking writes.
  • A spatial index (quadtree/geohash) makes “homes near here” a bounded query.
  • Per-listing availability calendars power both date filtering and booking.
  • Atomic, lock-guarded booking so overlapping stays can never double-book.
  • Payment hold-then-capture with idempotency keys for safe, retryable charges.
  • Event-driven reindexing and review aggregation keep search fresh asynchronously.
  • Read replicas, search caching and regional sharding scale the read-heavy load.

Concepts covered

  • What is Airbnb?
  • List and find homes
  • Geo indexing
  • Availability calendars
  • Transactional booking
  • Payment hold & capture
  • Reviews & reindexing
  • The sharp edges
built to be booked, not memorized — make the calls, drop the search, run the gauntlet.
Finished this one? 0 / 65 System Designs done
NextDesign a Key-Value Store Closest system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs