InterviewLegend

Design Booking & Reservations

Signature problem
Alex Chen45 min readUpdated 74 real interview reports
In this lesson
  • Scope a reservation system and pin the consistency requirement that drives every decision
  • Walk the browse, hold, confirm, and cancel paths end to end with a role-colored architecture
  • Go deep on the part that decides the round: stopping two guests from booking the same dates

Understanding the problem

The prompt

Build the system behind a booking: a guest finds an open place, holds the dates while they pay, and walks away with a confirmed reservation the host is committed to.

This is the single most-asked design in Airbnb reports, and the most Airbnb of all of them. The whole product is one booking transaction: a guest finds a place and holds the dates, they pay, and the host hands over a confirmed stay. Get the booking right and you have touched availability modeling, money, the multi-step payment workflow, and the one concurrency bug that defines the round.

The scale is not where the difficulty lives, so put the numbers down early and move past them. Airbnb runs on the order of 8 million active listings and around 450 million nights booked a year. That last figure is about 1.2 million booked nights a day. Even if every reservation were a single night, that is under 15 writes a second on average, and a few hundred a second at a holiday peak. Tiny. A laptop could keep up. What makes this hard is that every one of those writes has to be exactly right: a night sold once, a card charged once, a host never double-committed. This is a correctness problem in a scale problem's clothes. Treat it that way.

The Airbnb angle
This is a product-architecture round, so reason about the guest and the host, the two sides of every booking, and treat trust and money as first-class from the first box you draw. One thing draws the hardest pressure: two guests racing for the last open week on the same listing. Put "the write path is strongly consistent" on the table early and let every later choice fall out of it. Reach for eventual consistency on a payment the way you would for a feed, and you've just told the room you don't see the difference between a like and a charge. That's the answer that gets down-leveled.

Functional requirements

Propose a tight scope and let the interviewer redirect. The core is a short loop: a guest sees which dates are open and what they cost, holds a set of dates while they pay, and ends up with a confirmed reservation. Hosts set availability and price on the other side. Cancellation rounds it out, because a refund touches the same calendar and the same money the booking did.

In scope

  • Show a listing's open dates and nightly price
  • Hold dates while a guest checks out
  • Confirm a reservation and charge the guest exactly once
  • Let a host block dates and set price
  • Cancel a reservation and refund per policy

Below the line (call it out, then skip)

  • Search and ranking (its own design)
  • Host-guest messaging (its own design)
  • Reviews and the ratings system
  • Host payouts in full (we stop at scheduling one)
  • Fraud and trust scoring (named, then set aside)

Non-functional requirements

Reason through each one and attach a target, because here the requirements are the whole design. Correctness comes first, and it is non-negotiable. A set of dates on a listing can belong to at most one confirmed reservation, and a guest is charged exactly once per booking even when the network makes them click twice. The booking write path is strongly consistent. We would rather fail a booking and tell the guest to retry than sell the same week twice.

The read path is the opposite, and saying so scores. Browsing a calendar tolerates being a second or two stale. If a guest sees a date as open that someone grabbed half a second ago, the worst case is one failed booking attempt at the final step, which we handle cleanly. That relaxation lets us cache calendars hard and serve browse traffic, which dwarfs bookings, without ever touching the consistent core. Availability of the booking flow matters too, since a checkout that times out is a lost stay, but availability bows to correctness whenever the two collide.

  • ~8M Active listings. Each owns its own calendar.
  • ~1.2M Nights booked / day. ~450M a year, ~15 writes/sec average.
  • Exactly 1 Reservation per date. The invariant the round tests.
  • Strong Write consistency. Browse can be stale; the booking cannot.

The set-up

Core entities

Name the objects the system moves around before drawing anything. Four carry this design:

1

Listing

The place, owned by a host: an id, a location, plus the booking rules (max nights, how far ahead it can be booked, instant-book on or off).

2

Availability

A listing's calendar: for each date or date range, is it open, held, or booked, plus the nightly base price the host set. The source of truth the booking write fights over.

3

Reservation

One booking: a guest, a listing, a check-in and check-out date, a total price, plus a status that walks from requested to confirmed to cancelled.

4

Payment

The money attached to a reservation: an authorization, a capture, then later a refund or a host payout. Idempotency-keyed so a retry never charges twice.

One split matters most, between the calendar and the reservation. A calendar is a per-listing structure asked one question constantly ("are these nights open?") and it is the row everyone contends over. The reservation is the durable record of a completed booking, written once and read for receipts and trip management. Both want a relational store with real transactions. The invariant we care about is one reservation per date, and a database constraint plus a row lock enforce that on their own. Postgres is the name everyone in the room knows, and it gives us SELECT ... FOR UPDATE and unique constraints, inside a transaction that either commits whole or not at all. Partition both by listing_id so one listing's calendar and its reservations live on the same shard. A single booking then touches one shard in one transaction. Say that partition key out loud. The interviewer asks.

API

The booking flow is two steps on purpose: a hold, then a confirm. Splitting them is what lets a guest take ten minutes to enter a card without the dates leaking out from under them, and it is the seam where idempotency keys go. Both write calls carry an Idempotency-Key header so a client that retries after a timeout gets the original result back instead of a second hold or a second charge.

The interface
Request
the open dates and nightly prices for a window
GET /listings/{id}/calendar?from&to
{ ranges: [{ start, end, status, price }] }
price a specific stay (nights, fees, taxes, currency)
POST /listings/{id}/quote
{ quoteId, total, breakdown }
soft-hold the dates while the guest checks out
POST /holds
{ holdId, expiresAt }
Idempotency-Key; TTL ~10 min
turn a hold into a paid, confirmed reservation
POST /reservations
{ reservationId, status }
Idempotency-Key; body has holdId + payment
host blocks dates or sets a nightly price
PUT /listings/{id}/calendar
200
cancel and refund per the listing policy
POST /reservations/{id}/cancel
{ status, refund }

High-level design

Build it one path at a time, naming the bottleneck as each surfaces. The system splits along a consistency line rather than a read/write line. One path is a heavy, relaxed browse that can be cached and served stale. A small, strict booking path is the other, where every write has to be exactly right. The diagram codes each box by role, blue for people, violet for our services, amber for the event log, green for storage. The walkthrough below tags each component the same way.

Loading diagram...

1) Browse the calendar. The Guest calls GET /listings/{id}/calendar against the Calendar service, which serves the listing's open dates from a cache in front of the Availability DB. This is almost all of the traffic. For every booking, the same dates get viewed hundreds of times by guests scrolling search results and opening listings. Reads run on the order of thousands per second. The write path sees a few hundred per second at its worst. We let these reads lag the truth by a second or two on purpose. The only cost of showing a date that was grabbed half a second ago is one rejected attempt at the final confirm, which we handle cleanly. Serving browse from cache is what keeps this firehose off the date rows that the booking path locks and contends over, the load the second deep dive sizes.

2) Price the stay. When the guest picks dates, the client calls POST /listings/{id}/quote. The Pricing service reads the host's nightly base prices from the Availability DB, then adds the seasonal and demand adjustment, the cleaning and service fees, the local taxes, plus any currency conversion. It returns a quote that is frozen and stored under a quoteId for a short window, roughly ten to fifteen minutes. The frozen number is the exact price the guest will be charged. It can't drift if the host bumps the rate or the exchange rate moves between the guest seeing the total and paying it. The pricing math itself stays off the booking write path entirely, so a slow pricing call never holds a lock. The third deep dive builds the layered pricing model and the multi-currency case out.

3) Hold the dates. The guest calls POST /holds, and the Booking service asks the Calendar service to claim the dates. Inside one transaction it locks the listing's affected date rows, checks they are all still open, and flips them to held with a short expiry on the order of ten minutes. That whole locked write runs in a few milliseconds, so the lock is gone almost as soon as it is taken. The guest now owns those nights long enough to enter a card, and no one else can hold or book them. If the dates were already taken, the call fails right here, before any money is involved. The naive version of this step, a read-then-write with no lock, is exactly the double-booking bug, and untangling it is the first deep dive.

4) Confirm and pay. The guest submits payment and the client calls POST /reservations with the holdId and an Idempotency-Key. The Booking service checks the hold is still valid and asks the Payment orchestrator to charge the guest through the Payment gateway, a call that takes a second or two and can fail. On success it writes the Reservation DB row and flips the held dates to booked. Every step is keyed, so a retry cannot double-book or double-charge; then it drops one event on the Event log and returns. Everything after the charge happens off that event, never on the guest's request: the host payout, the confirmation emails, the search-index update, all of it, which keeps the booking transaction small. Sequencing the database write against an external charge and a calendar update with no distributed transaction is the fourth deep dive.

5) Host writes and cancellation. A Host opening or blocking dates hits the same locked calendar write a guest hold does, because a host block and a guest hold race for the exact same date rows. These writes are rare next to booking; a host edits a calendar a handful of times a week. They're strict for the same reason, though: a host must never block a night a guest already holds. A cancellation runs the booking in reverse: refund through the Payment orchestrator per the listing's policy, flip the dates back to open, and emit a cancellation event so the search index frees them again. Both paths reuse machinery the booking already built, the locked write and the keyed payment steps, which is the sign the core is modeled right. The reservation states this walks through are the fourth deep dive.

Capacity check

The write rate we already sized: about 1.2 million booked nights a day is roughly 15 bookings a second on average, and a few hundred a second at a holiday peak. Each one is a single short transaction on a single shard, so a commodity Postgres primary handles it with room to spare. The interesting number is the gap to the read side. For every booking, the same dates get viewed hundreds of times by guests browsing search and opening listings. Read traffic lands on the order of thousands per second, two to three orders of magnitude above the writes. That ratio is why browse gets its own cached, relaxed path and never touches the date rows a booking locks.

Storage grows slowly and we keep it forever. Around 450 million reservations a year at roughly 1KB each (guest, listing, dates, total, status, the frozen price breakdown) is about half a terabyte a year. The payment records attached to them add a comparable slice. After a decade that is single-digit terabytes, small enough that retention is never the constraint: we hold reservations indefinitely because a receipt or a dispute months later replays them. For the cache, a listing's calendar stored as the compact date ranges of deep dive two is about a kilobyte. Caching all 8 million is on the order of 8GB, which fits in one Redis cluster and absorbs the entire browse firehose.

  • Write rate: ~15/sec avg. 1.2M booked nights a day, a few hundred per second at a holiday peak. Each is one short transaction on one shard, trivial for a single Postgres primary.
  • Read:write skew: ~100x. Hundreds of calendar views per booking puts browse in the thousands per second. That gap is why browse runs on a cached, second-stale path of its own.
  • Reservation storage: ~0.5 TB/yr. 450M reservations a year at ~1KB each, plus payment records. Single-digit TB over a decade, kept indefinitely for receipts, refunds, and disputes.
  • Calendar cache: ~8 GB. 8M listings at ~1KB each as compact date ranges. Fits one Redis cluster and serves the browse firehose without touching the Availability DB.

Technology choices

Every box so far is a category. In the room, name a real tool for each and be ready for the failure question, because that is where this round goes. What happens when the payment gateway times out, or a guest closes the tab mid-hold, or two confirms land in the same millisecond?

  • The Booking service, Calendar service, and Pricing service run as stateless servers behind a load balancer. Stateless means no server keeps anything between requests that a later request depends on: no in-memory hold, no session, no "this guest is mid-checkout" flag living in process memory. We do that deliberately, because it makes the servers interchangeable: any box can serve any call, the load balancer spreads traffic evenly, and we scale out by adding boxes. The probe this invites is "what happens when a server dies mid-booking?" Because it held no state, nothing is lost: the in-flight transaction rolls back in the database and the client retries against another box. The idempotency keys live in Postgres, not in a server's memory, so the retry lands on the same key and returns the original result instead of booking a second time.
  • The Availability DB and Reservation DB are Postgres, partitioned by listing_id. A relational store fits because the invariant is a constraint. Row locks (SELECT ... FOR UPDATE) and a unique constraint on (listing, date) enforce one-reservation-per-date inside a transaction that commits whole or rolls back. Partitioning by listing keeps a booking on one shard, so it stays a fast local transaction. The failure question, a shard's primary dying mid-booking, is answered by a synchronous replica that gets promoted; an in-flight transaction that had not committed simply rolls back, and the guest retries.
  • Holds expire, and there are two honest ways to do it. Store the expiry as a column on the held rows and have the claim treat any hold past its expiry as open, plus a sweeper that flips abandoned holds back to open. Or keep holds in Redis with a native TTL so they vanish on their own. The column approach keeps one source of truth in Postgres, so a hold and a booking can't disagree across two stores. The consistent path prefers it, while Redis is the lighter option when hold volume is high. Either way, the expiry is what stops a closed browser tab from locking a week forever.
  • The Payment gateway is an external processor, and the name to put on the board is Stripe (or Adyen). It is the right category because we never want to hold raw card numbers or wire up our own connection to the card networks; the processor does the regulated, risky part. The Payment orchestrator wraps it and drives each charge as a sequence of idempotent, retryable steps. Authorize checks the funds exist and reserves them. Capture moves the money, and later comes a refund or a host payout. Every step carries the booking's idempotency key, so running it twice is a no-op. The probe this invites is the timeout. The gateway is slow and occasionally lies, returning an error on a capture that secretly went through. The orchestrator never assumes the outcome. It retries under the same key and reconciles against the processor's own record, so a timed-out capture is detected rather than charged a second time.
  • The Event log is Kafka, partitioned by reservation id. One confirmed booking writes one event, and notifications, payouts, and the search index each read it independently, so a slow email service can never wedge a booking. The log keeps history, so a consumer that crashes rewinds to its last bookmark and replays; events are keyed so a replayed booking event does not send a second confirmation email. To bridge the gap between committing the reservation and publishing the event, write the event to an outbox table in the same transaction. Relay it to Kafka after commit, and a crash between the two can never lose or invent a booking.
  • Browse reads sit behind Redis, keyed by listing and month, because a calendar gets read hundreds of times for every change. We already decided a browse may lag the truth by a second, so serving it from memory instead of Postgres costs almost nothing in correctness. We keep hit rates high with a longish TTL plus an active invalidation: the booking event refreshes the affected listing's cached months the instant its dates flip. The probe is the cache stampede, where a hot listing's entry expires and a thousand viewers miss at once and stampede the database together. The answer is to coalesce those misses so a single request refills the entry while the rest wait on its result. This is the cache that absorbs the browse firehose and keeps it off the Availability DB.
The Airbnb angle
Airbnb has written publicly about Orpheus, its payment orchestration framework, which models each payment workflow as a graph of idempotent, retryable steps so a failure anywhere can be retried safely without double-charging. You don't need to reproduce it, and don't claim internal detail you don't have. But name the shape: idempotent steps keyed so retries are safe, reconciled against the processor of record. That sounds like an engineer who has run payments, not one who has only diagrammed them. The same blog describes Viaduct, a data-oriented GraphQL layer their clients query, which is why the real client API here would be GraphQL rather than the plain REST routes we sketched. Either framing is fine; REST just keeps the booking sequence legible.

Potential deep dives

Most of the interview signal lives here. The interviewer picks one or two pieces and pushes. These are the ones Airbnb leans on, and the first one carries most of the round.

1) How do we stop two guests booking the same dates?

This is the signature Airbnb probe, and a strong loop can turn on it alone. Picture a cabin that is perfect for an eclipse week, and two guests both click Book on the last open dates inside the same second. The volume is nothing, one extra request, but both requests are aimed at the exact same date rows. The whole job is making sure precisely one of them wins, the other gets a clean "those dates are gone," and nobody is charged for a stay they will not get.

BadRead the calendar, then write the booking

Approach

Check the dates are open with a SELECT, and if they are, INSERT the reservation. It reads like the obvious thing and works perfectly in a demo with one user.

Challenges

Both racing requests run their SELECT before either has written. Both see the dates as open. Both INSERT. Now one listing has two confirmed reservations on the same week and two cards are charged, and you find out when a host has two families at the door. The gap between the read and the write is the entire bug, and nobody should propose this. Being able to explain *why* it fails is the point of the question.

GoodLock the rows inside one transaction

Approach

Wrap the claim in a transaction that locks the listing's affected date rows with SELECT ... FOR UPDATE before reading their status. The first request to arrive takes the lock; the second blocks until the first commits, then reads the dates as taken and is rejected. A unique constraint on (listing, date) is the backstop: even if two writes somehow slip through, the database refuses the second one. Because we partitioned by listing_id, all of this is one local transaction on one shard, which is why a write rate this low is a non-issue.

Challenges

The lock is held for the length of the transaction, so the claim must be quick: lock the dates, flip them to held, commit, all in milliseconds. The slow part, the guest entering a card, must happen *outside* the lock. That is exactly why we split hold from confirm: the hold takes and releases the lock fast, and the ten minutes of checkout happen against an already-held row that no lock is sitting on.

GreatSoft hold with a TTL, plus idempotency keys

Approach

On top of the lock sit two layers, starting with the soft hold: the fast locked write flips the dates to *held* with an expiry around ten minutes. The guest owns the nights long enough to pay, and everyone else sees them as taken. If the guest abandons checkout, the hold expires and the dates reopen on their own, no human and no stuck inventory. Second, idempotency keys: every POST /holds and POST /reservations carries a client-generated key, stored with the result. A guest on a flaky phone who taps Confirm twice, or a client retrying after a timeout, hits the same key. The second call returns the first call's result instead of making a second hold or charge. The race between two guests is settled by the lock; the race between a guest and their own retry is settled by the key.

Challenges

You now have to expire holds reliably (a TTL column the claim respects, plus a sweeper) and store idempotency keys with a sensible retention so they cover the retry window without growing forever. Both are small. This is the bar because it handles both real concurrency cases at once: two different guests, and one guest hitting submit twice. It never holds a database lock across a human's checkout.

The Airbnb angle
Expect to spend most of the round right here, so don't wait to be asked. Bring the soft hold and the idempotency key yourself, and name the two distinct races: two guests on the same dates, and one guest's double-tap. "We'd use a lock" is not an answer. Which rows does it lock? For how long? Where does the ten-minute checkout window sit relative to it? Lock the dates, flip them to held, commit in milliseconds, and let the human-speed checkout happen against an already-held row. That level of precision is the difference between sounding like you read about this and sounding like you've shipped it.

The handling contention lesson covers row locks and idempotency keys in depth, including the optimistic-concurrency variant.

2) How do we store and serve the availability calendar?

Size the obvious design first. A listing can be booked up to a year or two out, so call its calendar 730 days. Store one row per listing per day and you have 8 million listings times 730 days, about 6 billion rows, almost all of them just saying "open." That works, but it is a lot of rows to write a host block across and a lot to cache. The fix is to notice that a calendar is mostly long unbroken stretches of the same state.

GoodOne row per listing per day

Approach

A table keyed by (listing, date), each row carrying a status and a nightly price. Querying a stay is a range scan over a handful of dates, and booking flips a handful of rows. It is simple and the per-date price is easy to model, since price already lives per day.

Challenges

It is ~6 billion mostly-redundant rows, and a host opening "all of next summer" rewrites a hundred rows for one action. Cacheable, but heavy. For most listings, which sit open for long stretches, you are storing the same fact thousands of times.

GreatDate ranges, expanded only where they need to be

Approach

Store availability as ranges: a row says "March 1 to June 30, open, $120 a night." A booking splits a range (the booked nights become their own *booked* range, the open remainder stays open) and a host price change splits it the same way. A mostly-open listing is a few rows instead of 730. Reads are still a simple overlap query: find the ranges touching the requested window. Booking still locks and splits inside one transaction, so the concurrency story from deep dive one is unchanged; we have only made the rows fewer and the host edits cheaper. Keep the per-day price as an override layer for the days a host tuned individually, so the common case stays a range and only the exceptions expand.

Challenges

Range splitting is more code than flipping a flag. An off-by-one on an inclusive versus exclusive check-out date is a classic bug: the night of check-out isn't occupied, so ranges meet at the checkout date. The payoff is far fewer rows and cheap host edits, with a calendar small enough to cache per listing and serve the browse firehose from memory.

The two consistency levels meet here. The host block and the booking claim go through the strict locked write on the Availability DB. Browse reads come off the cache, refreshed on the booking event, and are allowed to lag by a second. That single decision, strict on the claim and relaxed on the browse, is what lets one calendar serve millions of casual viewers and a handful of contending bookers without either side slowing the other.

3) How does pricing handle multi-night stays and currency?

A nightly number on the calendar is not what a guest pays. A four-night August stay in a beach town is the sum of four nights that each cost differently. On top sit a cleaning fee, a service fee, taxes that depend on the city, and a conversion if the guest pays in a different currency than the host earns in. The interviewer wants two things. Separate the host's input from the computed total, and freeze that total at the right moment.

Model price in layers. The host sets a base nightly price per date, which lives on the calendar ranges from deep dive two. On top sits a pricing engine that applies adjustments. Seasonal and demand multipliers make a festival weekend cost more, and length-of-stay discounts make a week cheaper per night. The host's own rules apply too, and then come the fees and taxes, which are additive and depend on the listing's location. Last is currency, converted at quote time at a rate you record alongside the quote.

GreatQuote freezes the price; the charge replays it

Approach

POST /quote runs the pricing engine once and stores a frozen quote under a quoteId with a short lifetime: the total, the full breakdown, and the exact FX rate used. Checkout carries that quoteId into POST /reservations, and the charge bills the frozen total. The guest is charged exactly what they were shown, even if the host raises the price or the exchange rate moves in the ten minutes between seeing the number and paying. The breakdown is stored on the reservation so a receipt, a refund calculation, or a dispute all replay the same arithmetic.

Challenges

Quotes expire, so a guest who dawdles past the window gets re-priced, which is correct but needs a clear message. And demand-based pricing wants near-real-time signals (how many people are viewing these dates), which is a separate pipeline feeding the engine. The booking path never computes it inline.

The Airbnb angle
Pricing is where the two-sided product surfaces in the design. The host owns the base price and the rules; the platform layers demand pricing, discounts, fees, and currency on top. The follow-up that comes up most is multi-currency. The host is paid in their currency, the guest pays in theirs, and Airbnb eats the FX risk in the gap by locking the rate into the frozen quote. Record the exact rate on the quote and bill against it. The guest sees one number, the host sees another, and neither moves between the tap and the charge.

4) What is the reservation lifecycle, and how do we keep money and calendar in sync?

A reservation is a small state machine, and a booking is a saga across three systems that share no transaction: the calendar, the payment processor, and the reservation record. The interviewer wants the states named and the failure-in-the-middle handled, because that middle is where real money goes wrong.

The states walk in order. A booking is requested when the hold is placed. With instant book on, it goes straight toward payment; with request to book, it waits on the host and can be accepted or declined, with the hold keeping the dates safe in the meantime. On acceptance the card is authorized, then captured, and the reservation becomes confirmed. From confirmed it can later go cancelled (by guest or host) and refunded per policy. Each transition is a single, recorded step, never a flag flipped in two places at once.

Loading diagram...
GreatOrchestrated saga with compensating steps

Approach

No single transaction spans Postgres and an external card processor. Drive the booking as a saga: an ordered list of idempotent steps, with a compensating action if a later step fails. Authorize the card; if that fails, release the hold. Capture; if the reservation write then fails, void the capture. The Payment orchestrator owns this sequence and persists where it is in the list, so a crash mid-booking resumes from the last completed step rather than restarting and re-charging. Because every step is keyed, resuming and retrying are safe by construction.

Challenges

The hard case is the authorize or capture that times out, where the processor may have done it or not. The orchestrator cannot guess, so it reconciles, querying the processor under the idempotency key to learn the true outcome, and only then advances or compensates. Reconciliation, far more than the happy path, is what separates a real answer from a whiteboard one, and it is the operational mindset the senior bar is checking for.

The Airbnb angle
Orpheus is the perfect anchor here, and you can name it without overclaiming. Airbnb's published account of its payment orchestration is exactly this. Each workflow is a graph of idempotent steps that can be retried and resumed, precisely so a network blip during a charge never double-bills a guest or leaves a host unpaid. You can say "I would model the booking as an orchestrated saga of idempotent steps, the way Airbnb describes Orpheus, with reconciliation against the processor on a timeout." Naming the real system shows you know the domain; keeping it to the public shape keeps you honest.

The async processing lesson covers sagas and the outbox pattern that sit underneath this.

5) What breaks in production, and how does it recover?

Once the happy path is solid, a senior interviewer circles to the 4am questions: what fails, and does the system heal without a human. Each answer leans on a decision already made.

  • The guest closes the tab mid-checkout. Nothing leaks. The hold carries a TTL, so the dates reopen on their own when it expires, and the sweeper catches any hold the claim path missed. No stuck inventory, no support ticket.
  • The payment gateway times out on a capture. The orchestrator never assumes. It retries under the same idempotency key and reconciles against the processor's record, so a capture that secretly succeeded is detected rather than repeated, and one that truly failed releases the dates. A timed-out charge is the most dangerous failure in this system, and reconciliation is the main defense.
  • A shard's database primary dies mid-booking. Postgres promotes a synchronous replica. Any transaction that had not committed rolls back cleanly, which for a booking means the dates were never claimed and the guest simply retries. Partitioning by listing_id keeps the blast radius to one shard's listings.
  • The booking commits but the event never publishes. The outbox closes this dual-write gap. The booking event is written to an outbox table in the same transaction as the reservation, and a relay ships it to Kafka after commit. So a confirmed booking always has its event waiting, and a crash between the two cannot lose a notification or a payout, nor invent one for a booking that rolled back.
  • A downstream consumer falls behind or replays. Kafka keeps the log, so a crashed notification or payout consumer rewinds to its bookmark and catches up. Events are keyed by reservation, so a replayed booking event does not send a second confirmation email or schedule a second payout. Lag stays a freshness delay. Nothing is lost or duplicated.
  • A whole region goes down. Browse degrades gracefully on stale cache, which is acceptable for reads. The booking path needs its consistent store reachable, so replicate to a standby region with a clear, tested failover. A few in-flight bookings fail and retry, which beats risking a double-sell across regions.
The Airbnb angle
The senior bar is about the 3am question: when a piece fails, does a human get paged, and does the system heal on its own? Anyone can connect boxes on a clean day. This design is a gift for showing the harder thing, because every failure has an answer that already falls out of the core decisions. TTLs on holds. Idempotency keys on writes. Reconciliation on a payment timeout. An outbox for the event. Walk two or three of these before anyone asks: "here is what happens when the gateway times out, and here is why we still never double-charge." That shows the operational depth the level turns on.

What is expected at each level

That is a lot of surface area. Here is how much you need to land, by the level you are interviewing for. Airbnb's bar scales hard with level, and design is one of the rounds where the leveling committee adjusts a candidate up or down.

G8 (mid)

Breadth-led, about 80/20. Reach a working end-to-end booking: a calendar in a relational store, a two-step hold-then-confirm flow, a reservation record, a payment step. You should know what each box does when probed and you should recognize the double-booking risk when it is raised, even if the interviewer drives you to the locked-write fix. The bar: a complete flow you can explain, with the concurrency hole found together.

G9 (senior)

Now depth, roughly 60/40, and this is the level that most often turns on this design. You raise strong consistency on the write path before being asked, and you reach the soft-hold-plus-idempotency-key answer on your own, naming both races. You defend the partition key, and you handle a payment timeout with reconciliation rather than a hopeful retry. The bar: a near-optimal booking core where you found the concurrency and money failures yourself. More senior candidates are sorted here than anywhere.

G10+ (staff)

Depth leads as the scope widens, roughly 40/60. Expect the saga and its compensations, the outbox and the dual-write gap, multi-region failover and what you sacrifice to avoid a cross-region double-sell, and pricing as its own subsystem with demand signals feeding it. You are reasoning about how the system degrades and heals, and you are driving. The bar: you run the session and the interviewer steers rather than rescues, and you can defend every consistency choice as deliberate.

Key Takeaways

  • ✓Lead with consistency. The write rate is tiny, but the booking path must be strongly consistent: a set of dates belongs to exactly one reservation, and a card is charged exactly once.
  • ✓The two-step hold-then-confirm flow is the heart of it. A fast locked write places a soft hold with a TTL, and idempotency keys on both writes settle the guest-versus-retry race; the lock settles the guest-versus-guest race.
  • ✓Store the calendar as date ranges in Postgres partitioned by listing id, and serve browse from a cache that can lag a second. The host block and the booking claim stay on the strict locked write.
  • ✓A booking is a saga across calendar, payment, and reservation with no shared transaction. Drive it as ordered idempotent steps with compensations, reconcile against the processor on a timeout, and fan out via an outbox to Kafka, the shape Airbnb describes in Orpheus.
Had your Airbnb interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Airbnb interview guide

Know what Airbnb asks before you walk in.

18 coding · 5 system design · 20 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.