System Design Interview Crash Course
Vote

0% completed

​

Design AirBnB

1. Problem Definition and Scope

2. Clarify functional requirements

3. Clarify non-functional requirements

4. Back of the envelope estimates

5. API design

6. High level architecture

7. Data model

8. Core flows end to end

Flow 1: Guest Search (The "Read" Path)

Flow 2: Guest Creates Booking (The "Write" Path)

Flow 3: Asynchronous Synchronization (Updating the Search Index)

9. Caching and read performance

10. Storage, indexing and media

11. Scaling strategies

12. Reliability, failure handling and backpressure

13. Security, privacy and abuse

14. Bottlenecks and next steps

The complete picture

Summary

The decisions that mattered

The numbers that forced them

What breaks first

If the interviewer pushes

Image

This lesson designs Airbnb, a marketplace where people rent out their homes and other people book them for short stays. The design has two very different halves: a very large, very fast path for searching, and a small, careful path for booking. Almost every section below belongs to one of those two halves.

1. Problem Definition and Scope

We are designing a global online marketplace that connects Hosts, who want to rent out their properties, with Guests, who are looking for a place to stay.

Main User Groups:

  • Hosts: People who list properties and set the price and the available dates.
  • Guests: People who search for properties by location and date, view the details, and make reservations.

Scope:

We will focus on the core rental experience.

  • In-Scope: Listing management, search by location and date, viewing listing details, and the booking transaction flow.
  • Out of Scope: The details of payment processing (we assume a separate Payment Service), reviews and ratings, messaging between hosts and guests, and "Experiences" (paid activities).

In this design, a booking is confirmed as soon as the payment succeeds, with no approval step by the host. This is a choice made to keep the design simple. The real Airbnb also offers a request flow, where the host approves the guest first.

Image

2. Clarify functional requirements

Functional requirements list what the system must do for its users.

Must Have:

  • Host - Create Listing: Hosts can publish a listing with a title, description, price and photos.
  • Guest - Search: Guests can search for listings by location (city or coordinates), date range, and number of guests.
  • Guest - View Details: Guests can see the full property details, the amenities (such as WiFi or a pool), and information about the host.
  • Guest - Book: Guests can reserve a property for a specific date range.
  • Inventory Management: The system must strictly prevent double bookings, which means two guests booking the same property for the same dates.

Nice to Have:

  • Map View: An interactive map that shows the search results.
  • Dynamic Pricing: Hosts can set different prices for weekends or holidays.

Notice that the search and booking requirements have opposite needs. A search can show slightly old data as long as it is fast, while a booking must be correct even if it is slower.

Image

3. Clarify non-functional requirements

Non-functional requirements describe how well the system must work: how big, how fast, and how reliable.

Scale & Volume:

  • Users: 100 million monthly active users (MAU), meaning 100 million different people use the product at least once a month.
  • Listings: About 10 million active listings worldwide.
  • Read vs Write: The system is extremely read-heavy. There are about 1,000 searches for every 1 booking, a ratio of 1000:1. Section 4 shows where this number comes from.

Performance:

Latency is the time a user waits for a response.

  • Search Latency: Under 300 ms. Search is the main way guests discover a home, so it must feel immediate.
  • Booking Latency: About 2 seconds is acceptable. Most of that time is the call to the payment provider, and guests expect a payment to take a moment.

Consistency:

  • Search: Eventual consistency is acceptable. This means a change reaches every copy of the data after a short delay. If a host updates a description, a few seconds of delay in search results harms nobody.
  • Booking: Strong consistency is mandatory. Every booking decision is made on one up-to-date copy of the data, so two guests can never both receive the same dates. A double booking is a critical failure.

Availability:

  • Search: 99.99%, which still allows about 52 minutes of downtime a year.
  • Booking: 99.9%, which allows about 9 hours of downtime a year. A network partition is a failure that stops some servers from reaching others. During a partition, the Booking Service refuses requests that it cannot check safely, because a short error is better than a double booking.

The 1000:1 ratio is the most important number on this list. It tells us to spend our engineering effort on making reads fast, and it tells us that the booking path can afford careful, slower work.

Image

4. Back of the envelope estimates

These estimates are rough on purpose. Their job is to show which part of the system needs the most attention.

Traffic Estimates:

  • Daily Active Users (DAU): Assume 10% of the monthly users come on a normal day: 100 million × 10% = 10 million DAU.
  • Search Volume: Assume an average of 10 searches per user per day.
    • Per day: 10 million × 10 = 100 million searches a day.
    • Average: 100 million ÷ 86,400 seconds ≈ 1,200 queries per second (QPS).
    • Peak: Busy hours run about 5 times the average, so 1,200 × 5 = 6,000 QPS.
    • Cache lookups: Assume one results page shows 20 listings. At peak, 6,000 × 20 = 120,000 listing lookups per second, and section 9 serves them from a cache.
  • Booking Volume: Assume 1% of daily users make a booking.
    • Per day: 10 million × 1% = 100,000 bookings a day.
    • Average: 100,000 ÷ 86,400 seconds ≈ 1.2 bookings per second. At the same 5 times peak, that is about 6 per second.
    • Database writes: Flow 2 in section 8 writes each booking twice, once to reserve the dates and once to confirm them. That makes about 2.4 writes per second on average and 12 at peak. The write volume is very low, but the logic is critical.
    • Temporary holds: Flow 2 also sets one short-lived key in Redis for each night, so a 5 night stay sets 5 keys. This load is too small to matter.
  • Checking the ratio: 100 million searches ÷ 100,000 bookings = 1,000 searches per booking, which is the ratio from section 3.

Storage Estimates:

  • Listing Metadata: 10 million listings × 10 KB of text = 100 GB. This fits easily in one database, and most of it fits in memory.
  • Bookings: 100,000 bookings a day × about 1 KB per row, including indexes = 100 MB a day, or about 36 GB a year.
  • Images: 10 million listings × 10 photos × 2 MB = 200 TB of original photos.
    • Section 10 also stores resized copies of each photo. If the copies add about 1 MB per photo, that is another 100 TB, or about 300 TB in total.
    • This much data requires object storage, such as Amazon S3, and a Content Delivery Network (CDN), which is a large set of cache servers placed close to users in many cities.

Conclusion: Search traffic is thousands of queries per second, but bookings are only a few per second. That difference justifies two separate data stores: one built for fast search, and one built for correct transactions.

Image

5. API design

The API is the set of requests the app sends to our servers. We use a REST API, which means plain HTTP requests on named URLs.

1. Search Listings

  • GET /v1/listings/search
  • Params: lat, long, radius, check_in, check_out, guests, page.
  • Response: A JSON list of short listing summaries: id, title, price, thumbnail_url.
  • Note: When a guest types a city name, the app first converts it to coordinates with a map service, and then sends lat and long.

2. Get Listing Details

  • GET /v1/listings/{listing_id}
  • Response: The full details: photos, amenities, description and host information.

3. Create Booking

  • POST /v1/bookings
  • Body: { listing_id, check_in, check_out, payment_token, idempotency_key }
  • Response: { booking_id, status: "CONFIRMED" }
  • Errors: 409 Conflict if the dates are taken, and 402 Payment Required if the payment fails.
  • Note: The server reads the guest's identity from the login token, not from the request body. Otherwise, one user could make a booking in another user's name.
  • Note: payment_token is a short code from the payment provider that stands for the guest's card. idempotency_key is a unique ID that the app creates for this booking attempt. Section 12 explains how it prevents a double charge.

4. Create Listing

  • POST /v1/listings
  • Body: { title, description, location, max_guests, price, amenities, photos: [...] }
  • Note: photos holds references to files that are already uploaded, not the files themselves. The app first calls POST /v1/listings/photos, which returns a presigned URL, a temporary link that lets the app upload one file directly to object storage.
  • Note: Hosts change a listing with PUT /v1/listings/{listing_id}, and they block dates with POST /v1/listings/{listing_id}/blocks.

6. High level architecture

We use a microservices architecture, which means several small services that each own one job. The main reason is to separate Search, which is read-heavy and complex, from Booking, which is transactional and critical. A failure or a traffic spike in one should not harm the other.

Component Roles:

  1. API Gateway and Load Balancer: The load balancer spreads requests across many servers. The gateway checks who the user is and limits how many requests one client can send.
  2. Search Service: Handles search queries. It is backed by Elasticsearch (ES), a search engine with strong support for location and text queries.
  3. Booking Service: Handles the reservation logic. It reads and writes only the PostgreSQL primary, the one database copy that accepts writes. Postgres provides ACID transactions, which means a group of steps either all happen or none happen, and overlapping requests cannot interfere with each other.
  4. Listing Service: Serves property details. It writes to the Postgres primary, and it reads from Redis, an in-memory cache, and from read replicas, which are read-only copies of the primary.
  5. Payment Service: An external provider that charges the guest's card.
  6. Object Storage (S3) and CDN (CloudFront): S3 stores the photos. The CDN serves them to users, which lowers both latency and the traffic on our own servers. The app loads photos straight from the CDN, so these requests never pass through the API Gateway.
  7. Change pipeline: Debezium reads every committed change from the Postgres primary, and Kafka, a durable message log, carries those changes. The Indexer Service then applies them to Elasticsearch, so the search index follows the database without either service writing to both.

The Booking Service and the Listing Service share one Postgres database. This is less separated than a pure microservices design, and it is a deliberate cost. The Booking Service has to lock a listing row and check its bookings in one transaction, and a transaction cannot easily span two databases.

Image

7. Data model

We use PostgreSQL as the source of truth, because bookings require transactions. The source of truth is the copy that is correct if any copies disagree. We use Elasticsearch as a secondary index for search, which means its contents can always be rebuilt from Postgres.

Relational Schema (Postgres):

  1. Users
    • id (primary key), name, email, password_hash.
  2. Listings
    • id (primary key), host_id, title, description, price, max_guests, lat, long, public_lat, public_long.
    • Index: host_id.
    • public_lat and public_long hold the approximate location that section 13 explains.
  3. Bookings
    • id (primary key), listing_id, guest_id, start_date, end_date, total_price, status, expires_at, idempotency_key.
    • status is one of PENDING, CONFIRMED, FAILED, EXPIRED, CANCELLED or BLOCKED.
    • end_date is the check-out day. The guest does not stay that night, so another guest can check in on the same day.
    • Indexes: (listing_id, start_date, end_date) for the overlap check, and a unique index on (listing_id, idempotency_key) so that a repeated request cannot create a second row.
    • When a host blocks dates, the Booking Service inserts a row with status BLOCKED. The overlap check and the search index then treat it exactly like a booking.

Search Document (Elasticsearch):

We denormalize the data, which means we copy fields from several places into one JSON document, so that a search needs no joins. In the example, YYYY stands for the year.

{ "id": 101, "location": { "lat": 40.7, "lon": -74.0 }, "max_guests": 4, "amenities": ["wifi", "pool"], "booked_dates": ["YYYY-10-01", "YYYY-10-02", "YYYY-10-03", "YYYY-10-04"] }
  • Note: booked_dates lists the booked nights. A stay from October 1 to October 5 covers the nights of October 1 to 4, so October 5 stays free for the next guest.
  • Note: Storing booked_dates in Elasticsearch lets us filter out most unavailable homes during the search, without a database call for each result. Postgres still makes the final decision at booking time.

8. Core flows end to end

In a large system like Airbnb, one server rarely handles a whole request. Instead, a request passes through several services in turn.

This section follows the three most important flows: Search (Read), Booking (Write), and Synchronization (Async). For each step, we describe what happens, why the step is needed, and what it produces.

Flow 1: Guest Search (The "Read" Path)

Goal: Return relevant listings in under 300 ms, even if the data is a few seconds out of date.

This flow values latency and availability over strict consistency.

Step 1: Request Ingestion

What happens:

  • The client (mobile app or browser) sends a GET request to the API, for example ?lat=40.7128&long=-74.0060&radius=10km&check_in=YYYY-10-01&check_out=YYYY-10-05&guests=2.
  • The load balancer and the API Gateway receive the request, apply the rate limit, and send it to the Search Service.

Why: The gateway stops scrapers and overload before they reach the search system, and the load balancer lets us add Search Service machines as traffic grows.

Outcome: One Search Service machine now holds a checked request with a location, dates and a number of guests.

Step 2: The Two-Stage Query Strategy

What happens: The search runs in two stages: first find the matching IDs, then fetch the content. The first stage goes to Elasticsearch, not to Postgres. The Search Service sends one query with these filters:

  • Filter 1 (Location): Find listings whose location is within 10 km of the coordinates.
  • Filter 2 (Availability): Exclude listings whose booked_dates contain any requested night. A home in the index is not guaranteed to be free, because the index runs a few seconds behind. Flow 2 makes the final check.
  • Filter 3 (Guests): Keep only listings whose max_guests is at least the requested number.
  • Result: Elasticsearch returns one page of listing IDs, for example [101, 102, 105]. It does not return full descriptions, amenities or photo URLs, which keeps its response small.

Why: Postgres can answer location queries with its PostGIS extension, so this is not a question of what it can store. The problem is scale. Combining location, dates and other filters at 6,000 QPS would put a large load on the same database that must make bookings correctly. Elasticsearch is built for this type of query, and it can be scaled separately.

Outcome: The Search Service has a short list of IDs for listings that match and appear to be free.

Step 3: Data Hydration (Redis + Database)

Hydration means filling in the full content for a list of IDs.

What happens:

  • The Search Service sends the page of IDs to the Listing Service as one batch request.
  • Hit: For each ID found in the Redis cache, the service returns the title, price and thumbnail URL from memory.
  • Miss: For each ID not in Redis, the service reads the row from a Postgres read replica and saves the result in Redis.

Why: At peak, this step needs about 120,000 listing lookups per second. A cache in memory can answer those far more cheaply than the database. Listing details also change rarely, so the same cached copy can serve many searches.

Outcome: Every ID on the page now has the content that the results page must show.

Step 4: Response

What happens: The Search Service combines the results, keeps the order from Elasticsearch, and returns them to the client. The app then loads the thumbnail images from the CDN, not from our API servers.

Why: Keeping the content outside the index keeps the search engine small and fast, and it means one listing has one authoritative record of its details. This pattern is often called "query then fetch".

Outcome: The guest sees a page of listings, usually within the 300 ms target.

Image

Flow 2: Guest Creates Booking (The "Write" Path)

Goal: Ensure that no two people book the same home for the same night.

This flow puts consistency above everything else. We must prevent a race condition, a bug in which the result depends on which of two simultaneous requests runs first. Here, that would mean User A and User B both paying for the same home.

Step 1: Reservation Request

What happens:

  • The user clicks "Book". The app sends a POST request with an idempotency_key, a unique ID that it creates once for this attempt, for example uuid-123. A retry of the same attempt sends the same key.
  • The request passes through the API Gateway, which identifies the guest from the login token, and reaches the Booking Service.
  • The Booking Service looks up the key in the Bookings table. If it has seen the key before, it returns the stored result and stops.

Why: Mobile networks drop responses. The app cannot tell whether a failed request was processed, so it must be able to retry safely.

Outcome: The Booking Service knows that this is a new attempt, and it knows which guest made it.

Step 2: Temporary Hold (Redis)

What happens:

  • The Booking Service tries to set one Redis key for each requested night, such as hold:101:10-01, with a time to live (TTL) of 5 minutes. A TTL makes the key delete itself after that time.
  • A short Lua script, which Redis runs as one uninterrupted operation, sets all the keys or none of them. Each key stores the idempotency key, so a retry of the same attempt does not block itself.
  • If another attempt already holds any of those nights, the guest receives 409 Conflict immediately, with the message "Someone is booking this".
  • If Redis is unavailable, the Booking Service skips this step and continues.

Why: When many guests want the same home at the same moment, most of them get an answer here without touching the database. The keys are per night because two different date ranges can overlap. A single key for "October 1 to 5" would not stop a booking for "October 3 to 7".

Outcome: The requested nights are marked as busy for up to 5 minutes. This hold is only a fast first filter, and Postgres still makes the decision.

Step 3: The Transaction Boundary (PostgreSQL)

What happens: The Booking Service opens a short database transaction on the primary. It locks the listing row, checks for overlapping bookings, and inserts a row with status PENDING. Then it commits.

BEGIN; -- Lock the listing row. Other booking attempts for listing 101 wait here. SELECT id, price FROM listings WHERE id = 101 FOR UPDATE; -- Look for any active booking that overlaps the requested nights. SELECT count(*) FROM bookings WHERE listing_id = 101 AND start_date < :requested_end AND end_date > :requested_start AND (status IN ('CONFIRMED', 'BLOCKED') OR (status = 'PENDING' AND expires_at > now())); -- If count > 0: ROLLBACK and return 409 Conflict. -- If count = 0: reserve the dates and commit. INSERT INTO bookings (listing_id, guest_id, start_date, end_date, total_price, status, expires_at, idempotency_key) VALUES (101, :guest_id, :requested_start, :requested_end, :total_price, 'PENDING', now() + interval '5 minutes', :idempotency_key); COMMIT;

There are two main ways to protect a row. Optimistic locking lets everyone proceed and rejects a write if the row changed in the meantime. Pessimistic locking makes others wait before they start. For a system with sudden, extreme demand on one item, such as ticket sales, many requests would queue on one lock. A home is booked far less often, so we use pessimistic locking with SELECT ... FOR UPDATE.

This lock blocks other FOR UPDATE statements and writes to that listing row, so a second booking attempt must wait. Ordinary reads of the row are not blocked, because Postgres serves them from an earlier version. Once the first transaction commits, the second one gets the lock. Under the default isolation level of Postgres, its overlap query then sees the new row, so it rolls back.

Why: The lock and the check together make the insert conditional: the row is written only if these dates are still free. Two guests can never both pass the check. The price comes from the locked row on the primary, so the guest cannot be charged based on an outdated copy in the cache. Committing here matters too, because the lock is released after a few milliseconds, before the slow payment call begins.

Outcome: The dates are reserved by a durable PENDING row that stops counting after 5 minutes. One guest has inserted a row, and any competing guest has inserted none and received 409 Conflict.

Step 4: Payment Processing

What happens:

  • The Booking Service calls the external Payment Service (such as Stripe or PayPal). No database lock or transaction is open during this call.
  • It sends the payment_token and passes along the idempotency_key. Payment providers such as Stripe accept such a key and will not charge twice for it.
  • If the payment fails, the service sets the booking status to FAILED, deletes the Redis hold, and returns 402 Payment Required.

Why: A payment call takes seconds and can hang. Holding a row lock for that long would occupy a database connection and make every other booking attempt for this listing wait. The PENDING row protects the dates without blocking anything.

Outcome: The service knows whether the guest has paid.

Step 5: Commit

What happens: After a successful payment, the Booking Service confirms the booking in a second short transaction. This write is conditional too:

UPDATE bookings SET status = 'CONFIRMED' WHERE id = :booking_id AND status = 'PENDING' AND expires_at > now();

If the update changes one row, the home is booked. The service then deletes the Redis hold, and the user sees a "Success" screen. If the update changes no rows, the reservation expired during a very slow payment, so the service refunds the charge and returns 409 Conflict.

Why: The expiry time must be enforced at this point too. Without the condition, a late payment could confirm dates that another guest has reserved since the row expired.

A background job in the Booking Service handles the remaining failure. Each minute, it finds PENDING rows past their expiry time and marks them EXPIRED. If the service crashed between the payment and this update, the job finds the row, asks the provider about the charge using the idempotency key, and refunds any charge it finds.

Outcome: The booking is CONFIRMED, or the dates become free again and no money is kept. Each booking costs two short writes to Postgres instead of one, which section 4 counts.

Image

Flow 3: Asynchronous Synchronization (Updating the Search Index)

Goal: Update the search index so that other users stop seeing this home as available.

Immediately after Flow 2 finishes, PostgreSQL has the correct data, but Elasticsearch from Flow 1 is out of date. It still shows the home as free.

Step 1: Change Data Capture (CDC)

What happens: Change data capture means reading a database's own record of changes and passing them to other systems.

  • When the primary commits a CONFIRMED booking, it has already recorded the change in its Write-Ahead Log (WAL), the file where Postgres records every change before applying it.
  • A connector called Debezium reads the WAL and publishes an event to a Kafka topic named booking_events.
  • Payload: { "event": "BOOKING_CONFIRMED", "listing_id": 101, "dates": [...] }
  • Events are keyed by listing_id. Kafka keeps events with the same key in order, so a cancellation and a new booking of the same dates are applied in the order they happened.

Why: The Booking Service could write to Elasticsearch itself, which is called a dual write, but the two writes can half succeed. A crash between them leaves the index wrong with nothing left to correct it. With CDC, the index is built from what the database actually committed.

Outcome: Every committed change becomes one ordered event in Kafka, and the Booking Service did no extra work.

Step 2: Search Index Consumer

What happens:

  • A separate Indexer Service subscribes to the Kafka topic and reads each message.
  • For BOOKING_CONFIRMED and DATES_BLOCKED, it adds the nights to the booked_dates array in Elasticsearch. For BOOKING_CANCELLED, it removes them. It ignores PENDING rows, because they are short-lived and may never complete.
  • Kafka can deliver a message more than once after a failure. Adding the same night twice has no effect, because the Indexer Service treats booked_dates as a set.
  • Listing changes follow the same path on a second topic, listing_events. This is how a new listing or a changed description reaches search.

Why: A separate consumer means a slow or broken search index never delays a booking. The events simply wait in Kafka until the Indexer Service catches up.

Outcome: The Elasticsearch document now matches the database. Elasticsearch makes changes searchable at its next refresh, by default about once a second.

Step 3: Eventual Consistency

What happens: There is a lag of roughly 1 to 5 seconds between the database commit and the updated search result.

  • Scenario: User A books the home. Two seconds later, User B searches.
  • Edge Case: User B might still see the home in the results, because Elasticsearch is not updated yet.
  • Resolution: When User B clicks "Book", Flow 2 finds the overlap and rejects the request with 409 Conflict. The app then sends User B back to the search results.

Why: We accept this lag because the only copy that decides a booking is Postgres, and it is always current. The cost is an occasional disappointed guest, not a double booking.

Outcome: Search stays fast and scalable, and the booking guarantee does not depend on the index.

Image

9. Caching and read performance

1. Listing Details (Redis):

  • Key: listing:{id}.
  • Value: The full listing details as JSON.
  • TTL: 1 hour.
  • Strategy: Cache-aside, which means the service checks Redis first, and on a miss it reads the database and saves the result in Redis. When a host updates a listing, the Listing Service deletes the key.
  • Limit: A read replica can be a moment behind the primary. A read just after the delete could therefore save the old value again. The 1 hour TTL limits how long such a value can last, and the Booking Service never relies on cached prices.

2. Image Caching (CDN):

  • Images are large, at about 2 MB for an original. Serving them from API servers would use up our bandwidth and keep the servers busy.
  • The photos are stored in S3, and the CDN caches them on servers close to users.
  • The browser also keeps a local copy, using Cache-Control: max-age=31536000, which means one year. Because of this long lifetime, a changed photo gets a new file name, so the browser never shows the old one.

3. Search Availability:

  • We rely on the speed of Elasticsearch. We do not cache full search results, because the combinations of location, dates and guests vary too much for the same search to repeat often.

10. Storage, indexing and media

Primary Storage (Postgres):

  • Stores the authoritative state of bookings and listings.
  • Uses read replicas to handle the traffic of guests viewing listings. Booking decisions never read from a replica, because a replica can be slightly behind.

Search Index (Elasticsearch):

  • Uses geospatial indexing, an index built for finding "points inside an area" or "points within a radius". The general idea is to divide the map into smaller and smaller regions, so that a query skips most of the world. Geohash and quadtree are two well-known forms of this idea. Current versions of Elasticsearch store geo_point fields in a similar structure called a BKD tree.
  • The index is eventually consistent, and it runs 1 to 5 seconds behind the database. This is acceptable, because if a user tries to book a home that was just taken, the transaction in Flow 2 catches it.

Media:

  • A host uploads an image in three steps: the API generates a presigned S3 URL, the client uploads directly to S3, and the file is stored without passing through our servers.
  • S3 then triggers a Lambda function, a small piece of code that runs only when an event occurs. It creates resized versions (thumbnail, mobile and desktop) and updates the listing's record in the database.

11. Scaling strategies

Database Sharding:

Sharding means splitting one table across several database machines.

  • By our own estimates, this is not urgent. As a rule of thumb, a single Postgres machine comfortably holds about 1 TB. With 100 GB of listings and about 36 GB of new bookings a year, we would not reach that size for many years. A rate of 12 writes per second at peak is also far below the limit of one machine.
  • We plan for sharding anyway, for two reasons: one machine failure should not stop every booking in the world, and real growth may exceed these rough numbers.
  • Shard Key: listing_id.
  • All data for Listing 101, both its details and its bookings, lives on the same shard. This keeps the locking transaction on one machine, so it stays fast and simple.
  • What it costs: Questions that are not about one listing become harder. "All listings of this host" and "all trips of this guest" would have to ask every shard. We therefore keep two small lookup tables, one keyed by host_id and one keyed by guest_id, which list the matching IDs. The Users table is not sharded by listing, so the database cannot enforce links between users and bookings across machines. The services must check those links themselves.

Search Scaling:

  • Elasticsearch is a distributed system, so we can split the index into parts.
  • Strategy: Partition the index by geographic region, for example index-us-east and index-europe. Most searches name a location, so most queries touch only one region. A search near a border queries both neighboring regions.

Handling "Hot" Listings:

A hot listing is one that many guests try to book at the same moment.

  • If a listing suddenly becomes very popular, many requests could queue for the same SELECT ... FOR UPDATE lock.
  • Improvement: The temporary hold in Redis from Flow 2, Step 2 handles this. While one guest is checking out, the others see "Someone is booking this" at once. They never reach the database, which saves its connections for the attempts that can succeed.

12. Reliability, failure handling and backpressure

Idempotency:

An operation is idempotent if repeating it has the same effect as doing it once.

  • This is crucial for payments.
  • The client generates one idempotency_key for a booking attempt, and it reuses that key for every retry.
  • The Booking Service checks whether it has already processed this key. If it has, it returns the stored result. The unique index on the Bookings table also stops two simultaneous retries from creating two rows.
  • The same key goes to the payment provider. Together, these two checks prevent a double charge when the network times out.

Backpressure:

Backpressure means slowing down the intake of work when the part that processes it is full.

  • Kafka provides this for the search index. If Elasticsearch slows down, the Indexer Service reads events more slowly and the events wait in Kafka. The cost is more lag in search results, and no event is lost.

Circuit Breakers:

A circuit breaker stops calling a service that is failing, and it returns a fallback answer instead.

  • If Elasticsearch fails, the Search Service detects the timeouts and stops sending it queries for a short time.
  • Fallback: It shows a list of popular homes from Redis. This list cannot filter by the guest's dates, but the booking check in Flow 2 still protects against a double booking. We do not fall back to Postgres, because 6,000 QPS of search traffic could then overload the database that bookings depend on.

Graceful Degradation:

Graceful degradation means continuing with reduced features instead of failing completely.

  • If the Payment Service is down, guests can still search and view listings. Only the booking request fails, with a clear error, and the dates are released. This is the benefit of keeping search and booking separate.

13. Security, privacy and abuse

Security:

  • HTTPS is used everywhere.
  • PCI-DSS: This is the security standard for handling card data. We avoid most of its requirements by never touching raw card numbers. A form from the provider, such as Stripe Elements, collects the card details and gives us a token instead, which is called tokenization.
  • Identity: The Booking Service reads the guest's identity from the login token, so one user cannot make a booking for another.

Privacy:

  • Location Fuzzing: We store the exact location, but until a booking is confirmed, the API returns only an approximate one, a point within 500 m of the home. This protects the safety of hosts. We choose this point once and store it. If we chose a new random point for each request, someone could average many responses and find the real address.

Abuse:

  • Rate Limiting: We limit calls to /v1/listings/search to prevent scraping, the automatic copying of our data. We use a token bucket stored in Redis: each client has a bucket of tokens that refills at a fixed rate, each request uses one token, and a request with no token left is rejected.
  • Fraud: Background workers analyze booking patterns, such as one guest booking 5 houses for the same night, and flag them for manual review.

14. Bottlenecks and next steps

Bottleneck: Index Synchronization Lag

  • Issue: A user books a home, and the database is updated. For up to about 5 seconds, search still shows the home as available.
  • Mitigation: This lag can be made smaller, but it cannot be removed while the index is a separate system. We rely on the database check at the end of Flow 2 to catch it, and we monitor the lag so that we notice if it grows.

Bottleneck: Global Search

  • Issue: Searching "anywhere in the world" without a location is expensive, because the query must visit every regional part of the index.
  • Mitigation: This design asks users to select a region, such as "Europe", or it shows curated lists that are prepared in advance and stored in Redis.

The complete picture

Every diagram above shows one part of the system. This one shows all of them together: the search path, the booking path, the index synchronization and the media path, with the caches, queues, data stores and scaling strategy of each one marked. Notice that search reaches the Postgres primary only indirectly, through read replicas and the index synchronization, and that only the booking path makes booking decisions there.

The complete architecture, with caches, queues and scaling strategies marked
The complete architecture, with caches, queues and scaling strategies marked

Summary

Say this first. Airbnb is a read-heavy marketplace where search and booking want opposite things. Search wants speed and can accept data that is a few seconds old, so it runs on Elasticsearch. Booking must never sell the same night twice, so it runs as short, locked transactions on Postgres, with no lock held during payment. Almost every other decision in this design follows from that one split.

The decisions that mattered

DecisionWhyWhat it costs
Elasticsearch for search, Postgres for truthCombining location and availability filters at 6,000 QPS would overload the database that makes bookingsTwo stores that must be kept in sync
Pessimistic lock with SELECT FOR UPDATEOnly about 1.2 bookings per second across all listings, so two guests rarely want the same listing at onceRequests would queue on one lock in a ticket-sale style spike
Commit a PENDING row before calling the payment providerA payment takes seconds, and a lock held that long makes other attempts waitTwo database writes per booking instead of one, plus a job to clean up expired rows
Redis hold in front of the databaseMost competing guests get an answer without reaching PostgresOne Redis key per night, and a second copy of the reservation that can disagree, so Postgres stays the authority
Change data capture through Kafka, never dual writesA service that writes to Postgres and Elasticsearch itself can half failSearch runs 1 to 5 seconds behind
Get IDs from Elasticsearch, hydrate from RedisKeeps the search index small and its response lightOne extra hop on every search
Presigned uploads straight to S3200 TB of photos should never pass through an API serverThe client does more of the work

The numbers that forced them

NumberWhat it decided
1000:1 read to write ratioMake the read path as fast as possible. The write path is allowed to be slow.
6,000 QPS peak searchThis needs a search engine, not a transactional database.
About 1.2 bookings per secondLocking a row is affordable.
200 TB of imagesObject storage and a CDN, never the origin servers.
100 GB of listing metadataSmall enough that almost all of it can sit in cache, and small enough that sharding is not urgent.

What breaks first

  1. Index lag. A home that was just booked stays visible in search for a few seconds. The database check catches it at checkout, so a guest can get as far as pressing Book and then be rejected. That is the accepted trade-off, not a bug.
  2. A hot listing. Many guests try to book the same home at once. The Redis temporary hold turns most of those attempts into an instant "someone is booking this" instead of a queue on the database.
  3. Elasticsearch itself. The circuit breaker falls back to a list of popular homes from Redis. Reduced discovery is better than an error page.

If the interviewer pushes

  • "What if two guests book the same night at the same instant?" One of them gets the row lock first and commits a PENDING row. The other waits, then its overlap check finds that row, so its transaction rolls back. That guest receives a 409 and goes back to search.
  • "Why not hold the lock in Redis instead?" Redis replicates its data asynchronously, so a failover can lose a key. That is acceptable for a first filter and not acceptable for the final decision. If a hold is lost, the PENDING row in Postgres still protects the dates. A system selling concert tickets might do more of this work in Redis, because there the contention is the main problem.
  • "How do you avoid double charging on a retry?" The client generates an idempotency key before the first attempt. The Booking Service returns the stored result for a repeated key, and the payment provider receives the same key, so it does not charge again.
  • "What if the service crashes after the payment but before it confirms?" The PENDING row is already durable, and it expires after 5 minutes. A background job finds it, asks the provider about the charge, and refunds it.
  • "Where does this design fail?" Searching the whole world with no location. There is no cheap way to rank every listing on earth, which is why this design asks for a region first.
Rajat Singla

Rajat Singla

· 8 months ago

Each and every design is really awesome. A combined diagram with all the scaling strategies and caches in the end will really help readers absorb. Kudos!

Show 1 reply

Reading Progress

0%


Vote for new content

On This Page

1. Problem Definition and Scope

2. Clarify functional requirements

3. Clarify non-functional requirements

4. Back of the envelope estimates

5. API design

6. High level architecture

7. Data model

8. Core flows end to end

Flow 1: Guest Search (The "Read" Path)

Flow 2: Guest Creates Booking (The "Write" Path)

Flow 3: Asynchronous Synchronization (Updating the Search Index)

9. Caching and read performance

10. Storage, indexing and media

11. Scaling strategies

12. Reliability, failure handling and backpressure

13. Security, privacy and abuse

14. Bottlenecks and next steps

The complete picture

Summary

The decisions that mattered

The numbers that forced them

What breaks first

If the interviewer pushes