A stay is an unusual product. A guest does not buy a room; they buy a run of consecutive nights in it — 29 December to 2 January in a sea-view villa in Goa — and two guests collide only when their runs overlap. A home has one of each night to sell; a hotel has forty identical “Deluxe King” rooms per night and assigns the actual room on arrival. Guests search hundreds of times per booking, the same nights are often on sale on other sites, and the price depends on the dates, the length of stay and the currency. Airbnb alone reports more than 9 million active listings. You will design a marketplace in that mould — homes like Airbnb plus hotel inventory like Booking.com, MakeMyTrip or OYO — with search by place and dates, exact prices, instant booking with a short hold while the guest pays, cancellations, and the host tools that keep calendars right.
Understand the problem
Pin down two things before anything else. What is one unit of inventory? A Goa villa is one unit: each night is sold at most once. A Jaipur hotel’s “Deluxe King” room type is a pool of identical rooms: each night can be sold up to forty times, and the actual room is assigned at check-in. What does a booking claim? Not a timestamp range but a set of nights — check-in date included, check-out date excluded — in the property’s local calendar. Most of the design follows from those two answers.
Functional requirements
- Guests should be able to search for stays by place, dates and number of guests, with filters such as price, property type and amenities, as a list and on a map.
- Guests should be able to see a listing’s availability calendar and the exact total price for their dates before they commit.
- Guests should be able to book a stay and cancel it. Instant Book — which Airbnb has offered since 2010 — confirms as soon as payment is authorised; request-to-book waits for the host to accept.
- Hosts and hotels should be able to manage calendars, nightly prices and room inventory, directly or through a channel manager that also feeds other sites.
Non-functional requirements
- Never double-book. For every unit and night, held plus confirmed reservations never exceed the rooms available. Inventory is strongly consistent: an acknowledged hold is seen by every later attempt.
- Read-heavy, fast discovery. About 3,500 searches a second and 10,400 listing views a second at peak (derived below). Search p50 under 150 ms and p99 under 400 ms; listing page and price quote p99 under 300 ms.
- Fresh enough. Search may lag bookings by seconds (p99 under 10 s); the listing page, quote and booking always read the source of truth.
- Quick, safe booking. A hold answers in under 500 ms at p99, excluding payment. A guest is charged at most once per reservation, however often the app retries.
- The guest pays what they saw. A quote is honoured for 30 minutes, including its currency conversion.
- Availability. 99.99% for search and listing pages; 99.95% for booking, which may slow down but never trades correctness for uptime. RPO 0 for reservations and payments.
- Scale. 10 million bookable units with a year of calendar each, and about 1.5 million bookings a day in peak season.
In the interview
Ask four questions before drawing: “Homes, hotels or both?” (one slot per night, or a counter), “Instant Book, request-to-book or both?”, “Do hosts sell the same nights on other sites?” (whether you need channel sync), and “Do guests pay at booking or at the property?” (whether money waits for days or weeks). Then state the invariant: “For every unit and night, reserved never exceeds total.”
Out of scope
- Payment internals. Card authorisation, UPI flows and reconciliation belong to design a payment system; host payouts to a double-entry ledger. Here you only care how payment interacts with a hold.
- Ranking models, beyond where they sit in the search path.
- Reviews, messaging, identity checks, trust and safety.
- Flights, experiences and packages.
- Single-event flash crowds. Millions of fans for one stadium show need a virtual waiting room, covered in design Ticketmaster; stays demand is spread over many properties and nights.
The set-up
Plan the approach
The product has two halves with different rules. The discovery side — search, listing pages, calendars, prices — sees about 267 reads per booking, may be seconds stale, and is indexed and cached aggressively. The booking side — holds, payment, confirmation, host updates — is a few hundred writes a second that must be exactly right. Meet each requirement simply, then spend the deep dives on modelling nights, searching with dates, holding during payment, syncing channels, pricing and hot dates.
Core entities
- Property. A home or a hotel: host, location, IANA time zone (such as
Asia/Kolkata), check-in time, cancellation policy. - Unit. What a guest books: a whole home (total 1) or a hotel room type such as “Deluxe King, sea view” (total 40), with capacity and amenities.
- Night inventory. One row per unit per night: total, reserved (held plus confirmed), a closed flag, the night’s price and minimum stay. This is the calendar — the table the whole design protects.
- Rate rules. Base price, weekend and seasonal adjustments, length-of-stay discounts and fees, expanded into nightly prices whenever a rule changes.
- Quote. A stored, signed price for one unit, dates and party: line items, exchange rate, total, expiry.
- Reservation. Guest, unit, check-in and check-out dates, rooms, status, quote, payment reference, hold expiry, source channel.
- Channel connection. An iCal feed URL or a channel-manager mapping for sharing inventory with other sites.
Keep nights as dates, not timestamps. The night of 2026-12-31 in Goa is a date on Goa’s calendar, whatever the clock on the guest’s phone in Toronto says.
API
Discovery is a handful of cacheable GETs. Search takes a place or a map viewport, dates and the party size:
GET /v1/search?place=goa&checkin=2026-12-29&checkout=2027-01-02&guests=4
&bbox=15.80,73.68,15.20,74.00&nightly_max_inr=15000&instant=true&cursor=eyJwIjoyfQ
{
"results": [
{ "listing_id": "lst_4711", "unit_id": "unt_4711", "title": "Sea-view villa, Candolim",
"approx_total_inr": 52000, "instant_book": true }
],
"next_cursor": "eyJwIjozfQ"
}
approx_total_inr is a hint from the index. The calendar and the quote are the real thing:
GET /v1/units/unt_4711/calendar?from=2026-12-01&to=2027-02-01
{
"time_zone": "Asia/Kolkata",
"nights": [
{ "date": "2026-12-30", "available": true, "price_inr": 10000, "min_nights": 2 },
{ "date": "2026-12-31", "available": true, "price_inr": 20000, "min_nights": 3 }
]
}
POST /v1/quotes
Content-Type: application/json
{ "unit_id": "unt_4711", "checkin": "2026-12-29", "checkout": "2027-01-02",
"guests": 4, "display_currency": "USD" }
{
"quote_id": "q_51Kd.hmac-3f9a",
"lines": [
{ "label": "4 nights", "inr": 52000 },
{ "label": "Cleaning fee", "inr": 2000 },
{ "label": "Taxes", "inr": 5400 }
],
"total_inr": 59400,
"display": { "currency": "USD", "amount": "675.00", "fx_inr_per_usd": 88.0 },
"expires_at": "2026-12-01T12:30:00+05:30"
}
Booking creates a reservation from a quote. The client sends an idempotency key so a retry after a dropped connection never creates a second hold or a second charge:
POST /v1/reservations
Idempotency-Key: 8b0d6c3e-1f4a-4c55-9d7e-2a6f0e9b4c11
Content-Type: application/json
{ "quote_id": "q_51Kd.hmac-3f9a", "mode": "instant", "payment_method": "pm_card_7Hx" }
{
"reservation_id": "res_8Hq2",
"status": "HELD",
"hold_expires_at": "2026-12-01T12:15:00+05:30",
"payment": { "action": "redirect", "url": "https://pay.example.com/3ds/…" }
}
It returns 201 Created; 409 Conflict if any night is gone, with the nights named; 410 Gone if the quote expired, so the app re-quotes and shows the difference. The rest of the surface:
GET /v1/reservations/res_8Hq2 status, nights, quote, payment state
POST /v1/reservations/res_8Hq2/cancel → { "status": "CANCELLED", "refund_inr": 47520 }
POST /v1/reservations/res_8Hq2/accept host accepts a request-to-book
PUT /v1/host/units/unt_4711/calendar [{ from, to, price_inr, closed, min_nights }]
PUT /v1/host/units/unt_9002/inventory [{ from, to, total }] (hotel room counts)
POST /v1/channel/ari batched availability, rate and count updates from channel managers
POST /internal/payments/webhook the PSP reports an outcome (signed, retried)
The refund follows the cancellation policy snapshotted on the reservation — 80% of ₹59,400 is ₹47,520 — not the host’s policy on the day of cancellation.
Back-of-envelope estimates
Assume a global marketplace with 9 million homes and 1 million hotel room types (for example 200,000 hotels × 5 room types): 10 million bookable units. (The method is in back-of-envelope estimation.)
Searches. 100 million a day: 100M ÷ 86,400 ≈ 1,160 searches/s on average. Evenings run about 3× that: ≈ 3,500 searches/s at peak.
Listing views. About three per search: 300M a day, 300M ÷ 86,400 ≈ 3,470/s, so ≈ 10,400 listing views/s at peak.
Bookings. 1.5 million a day in peak season: 1.5M ÷ 86,400 ≈ 17 bookings/s, about 52/s in the busiest hour (3×). Guests abandon checkouts and lose races, so assume two holds per booking: ≈ 105 holds/s at peak. At an average of 3.5 nights a stay, that is 105 × 3.5 ≈ 370 night-row updates/s.
Look-to-book. (100M searches + 300M listing views) ÷ 1.5M bookings ≈ 267 reads per booking.
Host and channel updates. Pricing tools and channel managers change calendars constantly. Two updates a day per unit is 20M a day: 20M ÷ 86,400 ≈ 230 updates/s (about 700/s at peak), each touching about 30 nights — 230 × 30 ≈ 6,900 night-row writes/s on an average day, nearly 20× the 370 that bookings write even at peak. Most inventory writes come from hosts, not guests.
Calendar storage. A year ahead: 10M × 365 = 3.65 billion night rows. At about 80 bytes each with row overhead, 3.65B × 80 B ≈ 292 GB, roughly 0.6 TB with the primary-key index; a two-year calendar doubles that. Over 16 shards: 0.6 TB ÷ 16 ≈ 37 GB per shard — sharded for write isolation, not because one machine could not hold it.
Availability bitmaps. One bit per night for a year is 365 ÷ 8 ≈ 46 bytes per unit, so 10M × 46 B ≈ 460 MB — the whole catalogue’s availability fits in memory on every search node.
Search index. About 2 KB per listing document: 10M × 2 KB = 20 GB per copy. Change rate: 17 bookings + 230 host updates ≈ 250 document updates/s on average.
Tip
The surprising numbers in a stays marketplace are the ratios: 267 reads per booking, and host tools writing far more night rows than guests do. Design the read path for the first and the inventory pipeline for the second.
High-level design
Build the design one requirement at a time. By the end it works on an ordinary day; the deep dives make it survive the busy ones.
Guests should be able to search for stays by place, dates and number of guests
A guest types “Goa”, picks 29 December to 2 January and four guests. A geocoder turns “Goa” into a bounding box (on the map, the viewport already is one). The search service asks a search index — Elasticsearch or OpenSearch — for listings whose location falls inside the box, that sleep four, match the filters and look free on those nights. Airbnb’s search team notes that most of its searches carry filters, above all geographic ones.
The index is a copy. An indexer consumes change events from the Listings DB (descriptions, amenities) and the Inventory DB (nights, prices) through change data capture and rewrites each listing’s document: location, capacity, amenities, an approximate nightly price and an availability summary.
Results come back 20 at a time with an approximate total each; moving the map sends a new bounding box.
Guests should be able to see a listing’s availability calendar and the exact total price
A click on a listing fans out to three services. The listing service returns details from the Listings DB (photos come from a CDN). The availability service returns two months of nights from a calendar cache — Redis, keyed by unit — that falls back to the Inventory DB on a miss and is invalidated by the change events that feed the index. The pricing service sums the nightly prices for the chosen dates, adds fees and taxes, and converts the currency.
The listing page computes prices without storing them; at 10,400 views a second, storing each would be waste. When the guest presses Reserve, the pricing service writes a quote with a 30-minute expiry to the quote store and returns a signed ID. The booking charges that quote and nothing else.
Guests should be able to book a stay and cancel it
The booking service turns a quote into a reservation in one transaction on the Inventory DB:
- Lock the unit’s rows for each night of the stay, in date order.
- Add one to
reservedon every night, but only wherereserved < totaland the night is open. If fewer rows change than the stay has nights, roll back and answer 409. - Insert a reservation in state
HELDwith a hold expiry 15 minutes away, and commit.
The nights are now taken by data, not by an open transaction, so the guest can take their time on a 3-D Secure page or in a UPI app. The payment service authorises the quoted amount with the reservation ID as its idempotency key. When that succeeds — at once for most cards, later by webhook for app-based methods — the booking service flips the reservation to CONFIRMED, but only if it is still HELD.
A sweeper runs every 30 seconds, marks HELD reservations past their expiry as EXPIRED and gives their nights back by decrementing reserved, locking nights in date order. Request-to-book uses the same transaction with a REQUESTED state and a longer expiry. Cancellation is the mirror image: one transaction marks the reservation CANCELLED and decrements every night, then the payment service refunds per the stored policy. Each state change also writes an event to an outbox table in the same transaction, so notifications and the index hear about it exactly when it commits.
Hosts and hotels should be able to manage calendars, nightly prices and room inventory
Hosts edit calendars in the host app; hotels and professional hosts connect a channel manager that sends availability, rates and inventory — ARI — to every site they sell on. Both land on the host and ARI API, which validates each change and writes it to the Inventory DB. Rate rules (“+20% on Fridays”) are expanded into nightly prices before they are written, so the booking path never evaluates rules.
Two rules keep host edits safe. Lowering a total must not strand existing guests, so the table carries CHECK (reserved <= total) and the API refuses to close a night with reservations. And channel messages can arrive out of order, so the API applies them in sequence per unit.
You now have a working marketplace, correct at modest scale. The deep dives cover what breaks when you push it.
Deep dives
How do we stop two guests booking overlapping nights?
Priya books the Candolim villa for 29 December to 2 January; a second later Arjun tries 31 December to 3 January. They overlap on two nights, so exactly one may win. At the Jaipur hotel, the 41st guest asking for a Deluxe King on New Year’s Eve must be refused.
Bad: check, then insert. Look for overlapping reservations (check_in < :checkout AND check_out > :checkin), find none, insert. Both requests run the check before either inserts, so both succeed — a lost update in the shape of a phantom, which PostgreSQL’s default read committed isolation does nothing to stop. Serializable isolation would abort one, at the price of retries on every busy night. “Count overlapping stays, compare with 40, insert” races the same way.
Good: let the database reject overlaps. For single-unit homes, store each active stay as a daterange with an exclusion constraint — PostgreSQL’s documentation uses almost this example for meeting rooms, with btree_gist letting the plain unit_id join the GiST constraint.
CREATE EXTENSION btree_gist;
CREATE TABLE active_stays (
reservation_id text PRIMARY KEY,
unit_id bigint NOT NULL,
nights daterange NOT NULL, -- [check-in, check-out)
EXCLUDE USING gist (unit_id WITH =, nights WITH &&)
);
INSERT INTO active_stays VALUES ('res_8Hq2', 4711, '[2026-12-29,2027-01-02)'); -- ok
INSERT INTO active_stays VALUES ('res_9Kw1', 4711, '[2026-12-31,2027-01-03)'); -- error: conflicts
daterange is canonically half-open, [), so a check-out on 2 January and a check-in on 2 January do not overlap — exactly how nights work. No buggy service can bypass the constraint. The costs: “no two may overlap” is a count of one, so it does nothing for a hotel’s 40 rooms; expired and cancelled stays must be deleted before they stop blocking; and the listing page’s calendar still has to be computed from ranges.
Great: one row per unit per night, with a counter. Make the calendar itself the thing you lock. A home has total = 1, a hotel room type total = 40; one table and one code path serve both.
CREATE TABLE night_inventory (
unit_id bigint NOT NULL,
night date NOT NULL, -- local date at the property
total smallint NOT NULL, -- 1 for a home, 40 for "Deluxe King"
reserved smallint NOT NULL DEFAULT 0, -- held + requested + confirmed
closed boolean NOT NULL DEFAULT false,
price_inr integer NOT NULL,
min_nights smallint NOT NULL DEFAULT 1,
PRIMARY KEY (unit_id, night),
CHECK (reserved BETWEEN 0 AND total)
);
BEGIN;
-- 1. Lock the stay's nights in one fixed order: unit, then date.
SELECT night FROM night_inventory
WHERE unit_id = 4711 AND night >= '2026-12-29' AND night < '2027-01-02'
ORDER BY night
FOR UPDATE;
-- 2. Claim a room on every night, but only where one is left.
UPDATE night_inventory
SET reserved = reserved + 1
WHERE unit_id = 4711 AND night >= '2026-12-29' AND night < '2027-01-02'
AND reserved < total AND NOT closed;
-- 4 rows: continue. Fewer: a night is full, closed or not on sale -> ROLLBACK, 409.
INSERT INTO reservations (id, unit_id, check_in, check_out, rooms, status, hold_expires_at, quote_id)
VALUES ('res_8Hq2', 4711, '2026-12-29', '2027-01-02', 1, 'HELD', now() + interval '15 minutes', 'q_51Kd');
COMMIT;
Why it is safe: if Arjun’s transaction reaches 31 December while Priya’s holds it, his waits. Under read committed, a waiting statement is re-checked against the newly committed row once hers commits, so his UPDATE sees reserved = total and skips that night — fewer rows than nights, so Arjun gets a 409. A night never opened has no row and fails the same count. The CHECK constraint is a second wall: code that forgets the WHERE clause gets an error, not an oversold night.
Lock in date order. Two transactions that lock the same nights in different orders — 30 then 31 December, and 31 then 30, say from a date change iterating a hash set — can deadlock. PostgreSQL detects the cycle and aborts one; its manual’s cure is to take locks in a consistent order. The ordered SELECT … FOR UPDATE does that, and every path that touches nights sorts by (unit_id, night) first.
Why counts are enough for hotels. A counter never picks a room, so could a three-night guest find no single room free for all three nights? No. For intervals, the rooms needed equal the most stays overlapping on any one night. Hand out rooms in check-in order, each to a room whose last guest has left; you only run out if more than 40 stays share a night, which the counter forbids. So hotels assign rooms near arrival, and booking stays a counter.
The costs: 3.65 billion rows for a year, row-lock waits on popular nights, and expiry cannot be lazy. With a status per seat you can treat an expired hold as free inside the WHERE clause, as the Ticketmaster design does; a counter cannot tell which units belong to stale holds, so the sweeper must decrement them. If it runs late, the system undersells for a few minutes — never oversells, the right direction to fail.
| Ranges + overlap check | daterange + exclusion constraint |
Per-night rows with a counter | |
|---|---|---|---|
| Double booking possible? | Yes, under read committed | No | No |
| 40 identical rooms? | Only with a racy count | No — at most one per night | Yes: reserved < total |
| Write per booking | One insert | One insert into a GiST index | One update per night + one insert |
| Calendar for a listing page | Compute gaps from ranges | Compute gaps from ranges | Read the rows directly |
| Per-night price and minimum stay | Another table | Another table | Same row |
| Expired holds | Filter by status | Delete to unblock | Sweeper decrements |
| Good fit | Never | Homes-only marketplaces, meeting rooms | Homes and hotels together |
How do we search by place and dates without reading every calendar?
Geo and filters are standard index work. Dates are the hard part: “free on all four nights” is a fact per listing that changes about 250 times a second across the catalogue. Airbnb’s engineers report the same pressure — a high rate of real-time price and availability updates was the reason one in-memory index structure they tried grew too large.
Bad: search, then ask the database. The index returns everything in the Goa box that sleeps four — say 6,000 listings — and the service checks each calendar in the Inventory DB: 3,500 × 6,000 = 21 million calendar reads a second. Checking page by page does not help much: in peak season most candidates are full, so pages come back half-empty.
Good: put availability into the index. Store each listing’s free nights in its document and filter on all requested nights in the query, with the minimum-stay rule. One query, full pages. You pay twice: every booking, expiry and host update rewrites the document — about 250 rewrites a second on average, several times that on busy evenings — so the indexer batches per listing; and results are still seconds stale.
Great: filter in stages, and verify only what the guest sees. Keep the index for what changes slowly — location, capacity, amenities, a price band — plus a coarse availability field. Hold precise availability as one 365-bit bitmap per unit in the search service’s memory: 460 MB for the whole catalogue, kept current from the change stream within about a second. Then:
- The index returns candidates for the box and filters (6,000).
- The search service ANDs each candidate’s bitmap with a mask of the requested nights and drops the misses (1,800 left). That is about one 64-bit AND per candidate — microseconds for all 6,000.
- The ranker orders the survivors and keeps the first page’s worth (50).
- The availability and pricing services verify and price only the 20 the guest will see, against the calendar cache and, on a miss, the Inventory DB.
Two more pieces make it fast. Cache result pages for popular combinations — map cell, dates, party, filters — for 30 to 60 seconds; in December, thousands of guests ask for Goa on exactly the same nights. And sort by price approximately: the index holds a typical nightly price, and a listing that turns out pricier than the filter is dropped at verification.
You gain a search that touches the inventory database 20 times per page instead of 6,000. You pay with a second copy of availability to keep fresh, and a rule everyone follows: the index is never asked whether a stay can be booked. The listing page and quote re-check, and the booking transaction is the only judge — eventual consistency for reads, strong consistency where money and beds are at stake.
How do we hold a stay while the guest pays, and never charge twice?
A card payment usually takes seconds, but a 3-D Secure challenge waits for an OTP, and app-based methods take longer. Airbnb’s write-up on local payment methods — its list includes instant bank payments such as UPI and Pix — describes a flow where the guest scans a QR code, pays in a wallet app, and the result arrives later by webhook. Meanwhile the nights must stay reserved, and a phone retrying on a patchy network must never cause a second charge.
Bad: keep the transaction open through payment, or charge first. Holding row locks until payment returns makes everyone who wants 31 December wait behind a guest typing an OTP; PostgreSQL’s manual calls holding a transaction open while waiting for user input a bad idea. Charging first is no better: if the claim then fails, you hold money for nights you cannot give.
Good: make the hold data with an expiry. This is the high-level design: the claim commits at once as HELD for 15 minutes; payment happens outside any transaction; confirmation is UPDATE reservations SET status = 'CONFIRMED' WHERE id = 'res_8Hq2' AND status = 'HELD', which updates nothing if the sweeper got there first. A payment that lands after expiry re-runs the claim; if the nights are gone, void or refund and tell the guest at once. The Ticketmaster design adds a separate paying state for this window; the idea carries over.
Great: idempotent end to end, and capture timed to the stay. Three mechanisms make retries harmless:
- The client’s idempotency key. The app generates a random key (a V4 UUID, as Stripe’s API guide suggests) per press of “Confirm and pay”. The booking service stores it with the first response; a retry gets the same reservation back, a reuse with a different body is rejected, and keys are pruned after a day.
- The reservation ID at the payment provider. Every authorisation, capture and refund uses
res_8Hq2as its idempotency key, so timed-out calls can be repeated; PSP retries and unknown outcomes are covered in design a payment system. - Conditional state changes. Confirm, cancel and expire are all
UPDATE … WHERE status = …, so a duplicate webhook finds nothing to do, and only the transaction that changed the state writes the outbox event.
Then decide when money moves, where stays differ from tickets. Stripe’s documentation puts the validity of an online card authorisation at about seven days (closer to five for some merchant-initiated Visa payments). A stay booked 60 days ahead cannot sit on one authorisation, so:
- Pay now. Authorise during the hold and capture once the reservation is confirmed. Cancellation refunds whatever the stored policy allows.
- Pay later or at the property. Save the payment method, then authorise a few days before check-in; Stripe notes that hotels often authorise in full before arrival and capture at check-out. If that authorisation fails, give the guest a day to fix it, then cancel and release the nights.
- Request-to-book. The reservation sits in
REQUESTEDwith its nights claimed for up to 24 hours, card authorised well inside the seven-day window. Accept captures; decline or silence voids and releases. The host never has to choose between a request and an instant booking that arrived meanwhile; you pay with nights locked away from other guests, so keep the window short.
You gain payments as slow as any bank needs without blocking anyone, and retries that never create a second hold or charge. You pay with a sweeper and a webhook path to operate, three idempotency mechanisms to test, and the occasional late payment refunded with an apology.
How do we avoid double bookings with other sites?
The Candolim villa is also on Booking.com and the host’s own website; a Jaipur hotel sells through five travel sites and its front desk. Each site keeps a copy of availability, and a double booking is two copies disagreeing at the wrong moment. The question is how fast they converge.
Bad: iCal calendar sync. Each site publishes an .ics feed of blocked dates that the others fetch now and then. Booking.com’s partner help is candid: calendar sync is not real time, can take up to two hours, and can lead to double bookings. It is only offered to properties with one unit per room type, because a feed of blocked dates can say “closed” but never “3 of 40 left”. You gain a two-minute setup; you pay with an exposure window of hours.
Good: pull faster, and check before confirming. Refresh feeds every 15 minutes, and for instant bookings of single-unit listings, fetch the host’s other feeds on the booking path. The window shrinks, but if 2 million listings import feeds, a 15-minute cycle is 2M ÷ 900 s ≈ 2,200 fetches a second against other companies’ servers; booking now depends on a third party’s uptime; the check still races the commit; and counts remain impossible.
Great: one owner of inventory that pushes changes. Hotels and professional hosts use a channel manager or property-management system that owns the counts, pushes availability, rates and inventory to every channel the moment they change, and hears about each reservation immediately. Google’s hotel ARI interface shows the model: partners push messages on change rather than being queried — OTA-style messages for per-date or length-of-stay rates, availability and restrictions, and room counts — under an account limit of 400 messages a second, so senders batch and coalesce.
Your side of the contract:
- Apply updates in order. Each message carries a per-unit sequence number; the API applies it only if it is newer than the last one applied. A stale “2 rooms left” after a fresh “0 left” would reopen sold nights.
- Idempotent, batched writes. A message may arrive twice; a 60-night batch is one transaction, locking nights in date order like every other writer.
- Push your own bookings out through the outbox as they commit, with retries.
The window shrinks from hours to seconds, so conflicts become rare, not impossible. That needs a policy: the earliest confirmed booking keeps the room, and the other guest is moved to a comparable property nearby at the seller’s cost, with compensation. Some hotels instead give each channel a fixed allotment — five of the forty rooms — which removes the race but strands rooms on the wrong channel.
| iCal pull | Faster pull + check at booking | Push from a channel manager | |
|---|---|---|---|
| Exposure window | Up to about 2 hours | About 15 minutes; seconds for checked bookings | Seconds |
| Can express “3 of 40 left” | No — single units only | No | Yes, as room counts |
| Load you create | Low | About 2,200 fetches/s for 2M listings | Messages only when something changes |
| On the booking path | Nothing | A third party’s feed | Nothing |
| Who uses it | Individual hosts | Platforms protecting instant bookings | Hotels, professional hosts |
How do we show a price the guest can trust?
A stay’s price is a computation, not a field. For the villa, 29 December to 2 January:
- Nightly prices. The host’s base is ₹8,000. A season rule (+25% from 20 December to 5 January) makes most nights ₹10,000; the host overrides New Year’s Eve to ₹20,000; Friday 1 January gets the season price plus a 20% weekend uplift, ₹12,000. Total for four nights: 10,000 + 10,000 + 20,000 + 12,000 = ₹52,000.
- Length of stay. Rates can depend on how long the guest stays — a weekly discount, say; Google’s ARI messages carry length-of-stay rates alongside per-date ones.
- Fees and taxes. A ₹2,000 cleaning fee, then taxes, whose rules belong to the jurisdiction, can depend on the nightly tariff and change over time — so compute them per night from a versioned rule set. With an illustrative 10%: (52,000 + 2,000) × 10% = ₹5,400, a total of ₹59,400.
- Currency. A guest paying in dollars sees ₹59,400 ÷ 88.00 (an illustrative rate) = $675.00. Keep amounts as integers in the smallest unit (paise, cents), convert once, round once.
Bad: trust the price from the search result or the client. The index’s price is an approximation, and anything the client sends can be edited.
Good: recompute on the server at booking time. Correct, but the number can change between screens — the host’s pricing tool moved, or the exchange rate did. The guest sees ₹59,400, then ₹61,200, abandons, and complains about bait pricing.
Great: a signed, expiring quote. On Reserve, the pricing service stores the quote — inputs, nightly breakdown, rule-set version, exchange rate, total, 30-minute expiry — and returns an ID signed with a server-side key so it cannot be forged. The booking charges exactly that quote; a later host price change does not affect it. If it has expired, the API returns 410 and the app re-quotes and shows the difference before charging.
The costs: a quote store with one write per Reserve press — about 1.5 per hold, roughly 160 a second at peak, easy for a small Redis cluster with key expiry — and honouring quotes the host would rather not. Keeping the window at 30 minutes keeps that small, and the 15-minute hold fits inside it, so nobody re-quotes halfway through paying.
How do we handle New Year’s Eve in Goa?
Some nights everyone wants: New Year’s Eve in Goa, a cricket final in Ahmedabad, Diwali in Udaipur. Suppose Goa is normally 1% of searches: 1% × 3,500 = 35/s. In the week before New Year it runs 20× that, 700 searches/s — a fifth of peak traffic, mostly for the same few nights. Meanwhile a resort’s 40 Deluxe Kings on 31 December accept exactly 40 claims and refuse everything after.
Bad: treat hot dates like any other. Identical searches are recomputed thousands of times, and because results are seconds stale, guests keep pressing Reserve on nights that just sold out. Say 200 attempts a second land on full nights: each locks three or four rows on one shard before failing, slowing the attempts that could still succeed.
Good: cache the reads and short-circuit the doomed writes. The page cache from the search deep dive pays off most here, and the calendar cache stays warm for hot listings. Add a sold-out flag per unit and night: when a claim leaves reserved = total, the booking service sets soldout:{unit}:{night} in Redis, and the change stream clears it when a hold expires or a booking is cancelled. The booking service checks the flags before opening a transaction and answers 409 from memory, with similar stays. A wrong flag is harmless: a missing one sends the attempt to the database, which decides correctly; a stale one costs a freed night a few seconds of sales.
Great: shape the demand and spread the load. Four cheap moves:
- Shard inventory by property, never by city. A property’s nights live on one shard, so a booking is a single-shard transaction, and hashing property IDs spreads Goa’s hottest resorts across all 16 shards (sharding a relational database).
- Limit hoarding. Cap a guest at two active holds, and shorten holds to 10 minutes on nights more than 90% sold.
- Refresh hot listings first. A priority lane in the indexer for recently booked listings clears stale “available” results in about a second.
- Pre-scale from a calendar. New Year, Diwali and announced finals are known months ahead; scale search and caches before the rush.
Notice what you do not need: a virtual waiting room. A stadium on-sale aims millions of fans at one event’s seats in the same minute, so Ticketmaster queues them before its database. Here the crowd is spread across thousands of properties, and each claim holds a night row for about 5 ms, so one hot night can absorb roughly 1 ÷ 0.005 s = 200 claims a second while only 40 can ever succeed. The cost of a hot date is the losers, and the flag absorbs them.
Every writer goes through the same door
Every writer — booking, sweeper, cancellation, host edits, channel updates, date changes — must lock nights in the same (unit, night) order and go through the same reserved < total check. A single “quick fix” script that writes reserved directly, or a channel update applied out of order, is all it takes to oversell the one night that matters most.
The final design
Discovery reads copies that may be seconds stale; every change to a night goes through one conditional transaction on the property’s shard; and every copy — index, caches, other channels — is refreshed from the change stream.
Follow one New Year booking through the system:
- Search. “Goa, 29 December to 2 January, four guests” reaches the search service — or the page cache. The index filters by the Goa box and party size; in-memory bitmaps drop listings not free on all four nights; the ranker keeps the best 50.
- Verify and price. The availability and pricing service checks the 20 results on screen against the calendar cache (or the Inventory DB) and prices them. On the listing page it shows the calendar and exact total; Reserve stores a signed 30-minute quote.
- Hold. The booking service checks the sold-out flags, then runs one transaction on the villa’s shard: lock the four nights in date order, increment
reservedwherereserved < total, insert a 15-minuteHELDreservation. Four rows or 409; the idempotency key makes a retry return the same hold. - Pay. The payment service authorises the quoted ₹59,400 with the reservation ID as its idempotency key — a card in seconds, or a redirect or QR flow that finishes later.
- Confirm. The PSP’s answer, direct or by webhook, reaches the booking service, which flips
HELDtoCONFIRMEDonly if the hold is live, writing an outbox event in the same transaction. A late success re-claims the nights or is voided. The sweeper releases unpaid holds every 30 seconds. - Propagate. The outbox and change feed flow through Kafka to the indexer (bitmaps and documents, hot listings first), to the calendar cache and sold-out flags, and to notifications.
- Host and channel updates. Calendar, price and room-count changes reach the host and ARI API, which applies them in order per unit through the same checked update; they propagate like bookings.
- Push out. Bookings made here are pushed to the property’s channel manager within seconds, so other sites close the same nights.
Check it against the requirements. Double booking is impossible because every claim is a conditional update under a CHECK constraint, with one lock order for every writer. Search answers about 3,500 queries a second from an index and in-memory bitmaps, touching the source of truth only for the 20 results on screen, and tolerates seconds of lag because the listing page, the quote and the booking re-check. A guest pays the quoted price for 30 minutes, at most once, through idempotency keys at both ends. Holds survive slow payments without open transactions, other channels converge in seconds rather than hours, and hot dates are absorbed by caches, sold-out flags and property-level sharding rather than a waiting room.
What interviewers look for by level
| Area | Mid-level | Senior | Staff |
|---|---|---|---|
| Requirements | Search, book, cancel | Separates stale-tolerant discovery from strongly consistent inventory; asks homes versus hotels | Adds channels, pay-later and request-to-book; defines the invariant and the freshness budget per surface |
| Inventory model | A bookings table with dates | Per-night rows or an exclusion constraint; explains the check-then-insert race | One counter table for homes and hotels, the CHECK constraint, why counts suffice for room assignment |
| Concurrency | “Use a transaction” | Conditional multi-night update, rowcount check, 409 | Lock ordering across every writer, read-committed re-evaluation, sweeper semantics with counters |
| Search | Geo query in Elasticsearch | Availability in the index via CDC; re-check on the listing page | Staged funnel with in-memory bitmaps, page caching, “the index is never the judge” |
| Holds and payment | A timeout somewhere | HELD with expiry, authorise then capture, webhooks | Idempotency at client and PSP, late payments, authorisation windows versus lead time, request-to-book holds |
| Channels | Not raised | iCal sync and its delay | Push-based ARI with ordering, outbound pushes, an overbooking and relocation policy |
| Pricing | Nightly price × nights | Server-side pricing with fees and taxes | Signed, expiring quotes; integer money; FX locked; versioned tax rules |
| Hot dates | Add caching | Caches plus per-property sharding | Sold-out flags, hold caps, priority reindexing, pre-scaling; explains why no waiting room is needed |
Follow-up questions
What if a guest wants to change their dates?
Treat it as one transaction that releases the old nights and claims the new ones, with all affected nights — old and new — locked together in date order. If any new night is full, roll back and the original booking stands untouched; there is never a moment where the guest has neither. Price the change with a fresh quote and charge or refund the difference, keeping the original cancellation policy unless the change is large enough to count as a new booking.
What if the guest and the property are in different time zones?
Store check-in and check-out as plain dates on the property’s calendar and never convert them through UTC — midnight in Goa is 18:30 the previous day in UTC, the classic source of off-by-one-night bugs. Use the property’s IANA time zone only to turn deadlines into instants: “free cancellation until 48 hours before 14:00 check-in” becomes a UTC timestamp for the timer. And decide “today” on the property’s calendar: at 23:30 on 30 December in New York it is already 10:00 on 31 December in Goa, so a check-in for 30 December is in the past there. Daylight-saving changes at properties in Europe or North America then only shift deadlines, never nights.
What if a hotel wants to overbook on purpose?
Hotels expect a few no-shows and late cancellations, so some sell slightly more rooms than they have. Model it as an allowance per room type and night — total becomes physical rooms plus, say, 2 on nights with a reliable no-show rate — rather than weakening the check. The invariant still holds against the inflated total. When everyone turns up, the hotel “walks” the last guest: moves them to a comparable hotel nearby and pays for it. The platform’s job is to make the allowance explicit, capped and visible, never an accident of slow sync.
What if guests search with flexible dates, like “a weekend in March”?
Airbnb notes that flexible-date search adds a real layer of complexity, because there is no single range to test. With bitmaps the check stays cheap: build a mask for each candidate window — the four Friday-to-Sunday weekends in March 2027 are four masks — and keep a listing if any mask fits, along with its minimum-stay rule. Price only the best window per listing for the results page, and quote exact dates once the guest picks one.
What if a host cancels a confirmed stay a week before arrival?
Cancel through the same transaction (release the nights, refund in full), then start a re-accommodation flow: search for comparable, available stays nearby for the same dates, hold the best one for the guest at no extra cost, and let them accept or take the refund. Host cancellations are rare but damaging, so record them against the host and block the cancelled nights on that listing so they cannot simply be resold at a higher price.
How to say it in the interview
“I’ll design a stays marketplace — homes like Airbnb and hotel rooms like Booking.com — with search by place and dates, exact prices, instant and request-to-book, and cancellation. The invariant is that for every unit and night, reserved never exceeds total. Scale is about 10 million units, 3,500 searches a second at peak and 1.5 million bookings a day, so roughly 267 reads per booking.
I model inventory as one row per unit per night. A home is a unit with total one; a hotel room type is a unit with total forty, so one table serves both. Booking is one transaction: lock the stay’s nights in date order, increment reserved where reserved is below total, and require one updated row per night, otherwise roll back and return 409. A CHECK constraint backs it up. The same transaction inserts a reservation in HELD state for 15 minutes, so payment happens without any open transaction, and a sweeper returns unpaid holds.
Payment is authorise-then-capture with idempotency keys at both ends: the client’s key on the booking request and the reservation ID at the PSP. Confirmation is a conditional state change. For stays far ahead I authorise near check-in, because card authorisations last only about a week.
Search uses Elasticsearch for the geo box and filters, then in-memory availability bitmaps — 46 bytes per unit, about 460 MB for everything — kept fresh from change data capture. Only the 20 results on screen are verified and priced against the source of truth; the index is never the judge.
Prices come from signed quotes that expire after 30 minutes, with the exchange rate locked. Other channels get pushed availability from a channel manager rather than two-hourly iCal pulls, applied in order per unit, and conflicts that still slip through are handled by a relocation policy.
For hot dates like New Year’s Eve in Goa: cached result pages, sold-out flags that reject doomed holds from memory, hold caps, and inventory sharded by property so hot resorts spread out. No waiting room is needed, because contention per night is bounded by the number of rooms.”
Concepts used in this design
- Contention and double booking — the pattern behind every hold in this design.
- Optimistic vs pessimistic locking — conditional updates instead of locks held through payment.
- Lost update problem — why check-then-insert double-books.
- Transaction isolation levels — what read committed does when two claims meet on one night.
- Database constraints — the exclusion constraint and the
CHECKon reserved rooms. - Sharding a relational database — inventory sharded by property for single-shard bookings.
- Change data capture — keeping the index, caches and flags in step with inventory.
- Transactional outbox — publishing confirmations exactly when they commit.
- Elasticsearch — geo filters and facets for the first search stage.
- Proximity search — finding stays inside a place or a map viewport.
- Scaling reads — page caches and copies for a 267-to-1 read load.
- Hot keys, cache stampede and invalidation — hot dates and sold-out flags.
- Strong vs eventual consistency — stale discovery, exact inventory.
- Idempotency — safe retries on booking and payment calls.
- Webhooks — payment results and channel updates that arrive later, sometimes twice.
- Design a payment system — authorisation, capture and refunds in depth.
References and further reading
- PostgreSQL documentation: range typespostgresql.orgThe daterange type and its canonical [) form, GiST indexes on ranges, and the EXCLUDE USING gist example (with btree_gist) that rejects overlapping reservations per room.
- PostgreSQL documentation: explicit lockingpostgresql.orgDeadlocks from row-level locks, taking locks in a consistent order, and why a transaction should not stay open while waiting for user input.
- PostgreSQL documentation: transaction isolationpostgresql.orgUnder read committed, an UPDATE that waited for a concurrent update re-evaluates its WHERE clause against the newly committed row.
- Booking.com Partner Hub: how to synchronise your calendars across channelspartner.booking.comiCal sync is not real time, can take up to two hours, can cause double bookings, and is only offered to properties with one unit per room type.
- Google Hotel Center: ARI (availability, rates and inventory) overviewdevelopers.google.comPush-on-change messages for rates, availability, restrictions and room counts in OTA format, per-date or length-of-stay rates, and a 400 messages-per-second account limit.
- Stripe documentation: place a hold on a payment methoddocs.stripe.comSeparate authorisation and capture, hotels authorising before arrival and capturing at check-out, and card authorisation windows of about 5–7 days online.
- Stripe API reference: idempotent requestsdocs.stripe.comClient-generated idempotency keys (V4 UUIDs suggested), replaying the saved first response, rejecting reused keys with different parameters, and pruning keys after 24 hours.
- Airbnb: about us (fast facts)news.airbnb.comMore than 9 million active listings, and Instant Book launching in November 2010.
- Embedding-Based Retrieval for Airbnb Searchairbnb.techSearches dominated by geographic filters, a high rate of real-time price and availability updates, flexible-date search, and retrieval that narrows candidates before ranking.
- Pay as a Local (Airbnb Engineering)airbnb.techLocal payment methods including instant bank payments such as UPI and Pix, and the asynchronous QR-code flow confirmed later by webhook.