System Design · Reservations

Design Airbnb / a Hotel Booking System

The calendar is the hard part: how to model availability per night, guarantee no two guests get the same room, search millions of listings by date, and price each night correctly.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 18 min read
Calendar grid over a miniature town of homes and hotels with booked nights highlighted, representing a booking system
TL;DR
  • Model availability as one row per (room type or listing, night). A booking for 3 nights touches 3 rows; all must succeed or none do.
  • Prevent double booking inside the database: a conditional update (reserved < total) across the nights in one transaction, a unique constraint per night, or a PostgreSQL exclusion constraint on date ranges.
  • Use a hold with TTL while the guest pays, then confirm or release. Payment is idempotent on the reservation ID.
  • Search is retrieve (geo + filters) → availability filter → rank. Keep a compact availability bitmap or calendar in the search layer, and always re-check at booking time.
  • Hotels can overbook by a forecast margin; unique homes can't. Booking is strongly consistent; search and listings are eventually consistent.

Booking systems look like ordinary CRUD until two people try to book the same Goa villa for New Year's Eve at the same second. The interesting parts of "Design Airbnb" or "Design a hotel reservation system" are almost entirely about dates: how you store them, how you lock them, how you search over them and how you price them. This guide puts those first and keeps the rest (profiles, reviews, messaging) brief.

Airbnb vs Hotel Inventory: Clarify First

Ask the interviewer which one they mean. The inventory model differs in a way that changes the schema:

AspectAirbnb-style (unique homes)Hotel / OTA (MakeMyTrip, Booking.com, OYO)
Unit of inventoryOne listing = one unitRoom type with a count (e.g. 40 Deluxe King rooms)
Specific unit chosen at booking?Yes, the listing itselfNo; a room number is assigned at check-in
OverbookingNot possibleCommon, based on no-show forecasts
Inventory owned byPlatform (host manages calendar on it)Hotel PMS; shared across many channels
Instant vs requestInstant Book or host approvalInstant

A clean design handles both: a hotel room type is a "listing" with total_units = 40; an Airbnb home is a listing with total_units = 1.

Requirements

Functional

  • Hosts/hotels create listings, set calendars (blocked dates, min stay), base prices and per-date overrides.
  • Guests search by location, dates, guests, price range and amenities; view a listing with its calendar and total price.
  • Book (instant or request-to-book), pay, cancel per the cancellation policy, get refunds.
  • Reviews and messaging (mention, but out of scope for depth).

Non-functional

  • No double bookings, ever. This is the correctness bar.
  • Search latency under ~300 ms; booking latency within a couple of seconds including payment authorisation.
  • High availability for search; booking may briefly refuse during failures rather than risk inconsistency.
  • Read-heavy: lookers vastly outnumber bookers.

Estimates

Assumptions for a large global platform:

  • 5 million listings (homes + hotel room types); calendar horizon 365 nights.
  • Calendar rows: 5M × 365 ≈ 1.8 billion listing-nights. At ~40 bytes per row (IDs, date, counts, price override) ≈ 75 GB plus indexes. Sharded relational storage handles this comfortably.
  • Searches: 100M/day ≈ 1,160 QPS average, ~5,000 QPS peak (evenings, long weekends, Diwali/New Year planning).
  • Listing page views: 300M/day ≈ 3,500 QPS.
  • Bookings: 1M/day ≈ 12/s average, maybe 100/s peak. Each touches ~3 nights on average.

Read:write is roughly 1,000:1. Writes are few but contended: the same popular listing for the same weekend can see many simultaneous attempts. The problem isn't throughput; it's correctness under contention and search over a date dimension.

API

REST API
GET  /v1/search?lat=15.55&lng=73.75&checkin=2026-12-29&checkout=2027-01-02
              &guests=4&min_price=3000&max_price=12000&amenities=pool,wifi&cursor=..
GET  /v1/listings/{id}?checkin=...&checkout=...      → details + price breakdown
GET  /v1/listings/{id}/calendar?from=2026-12-01&to=2027-02-28

POST /v1/quotes        { listing_id, checkin, checkout, guests }
                       → { quote_id, nightly: [...], fees, taxes, total, expires_at }
POST /v1/reservations  { quote_id, payment_method }   Idempotency-Key: ...
                       → { reservation_id, status: "held" | "confirmed" | "requested" }
POST /v1/reservations/{id}/cancel

PUT  /v1/host/listings/{id}/calendar   { dates: [...], available: false, price: 9500 }

The quote step freezes the price the guest saw, so a host changing the price mid-checkout doesn't change what the guest pays, and gives you a natural place to validate min-stay and occupancy rules before you touch inventory.

The Availability Calendar Model

There are two ways to store availability. Interviewers like it when you compare them before choosing.

Option A: derive availability from bookings

Store only reservations (listing_id, check_in, check_out). A listing is free for [in, out) if no active reservation overlaps: existing.check_in < :out AND existing.check_out > :in. Simple and compact, natural for unique homes. Weak spots: no place for per-night prices, blocked dates and min-stay rules; and counting overlapping reservations per night for a 40-room hotel is awkward.

Option B: one inventory row per night (recommended)

SQL
CREATE TABLE listing (
  listing_id     BIGINT PRIMARY KEY,
  host_id        BIGINT NOT NULL,
  kind           TEXT NOT NULL,            -- home | hotel_room_type
  total_units    INT NOT NULL DEFAULT 1,
  base_price     INT NOT NULL,             -- in paise / minor units
  currency       CHAR(3) NOT NULL,
  min_nights     SMALLINT NOT NULL DEFAULT 1,
  max_guests     SMALLINT NOT NULL,
  lat DOUBLE PRECISION, lng DOUBLE PRECISION
);

CREATE TABLE inventory_night (
  listing_id     BIGINT NOT NULL,
  night          DATE   NOT NULL,          -- the night starting on this date
  total_units    INT    NOT NULL,
  reserved_units INT    NOT NULL DEFAULT 0,
  blocked        BOOLEAN NOT NULL DEFAULT false,
  price_override INT,                       -- NULL => use pricing rules
  overbook_pct   SMALLINT NOT NULL DEFAULT 0,
  PRIMARY KEY (listing_id, night),
  CHECK (reserved_units >= 0)
);

CREATE TABLE reservation (
  reservation_id  BIGINT PRIMARY KEY,
  listing_id      BIGINT NOT NULL,
  guest_id        BIGINT NOT NULL,
  check_in        DATE NOT NULL,
  check_out       DATE NOT NULL,          -- exclusive
  units           INT  NOT NULL DEFAULT 1,
  status          TEXT NOT NULL,          -- held|confirmed|cancelled|expired|requested
  hold_expires_at TIMESTAMPTZ,
  total_amount    BIGINT NOT NULL,
  idempotency_key TEXT NOT NULL UNIQUE,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  CHECK (check_out > check_in)
);

Note the convention: a stay from 24 to 27 December consumes the nights of 24, 25 and 26. The check-out date is exclusive. Getting this wrong is the most common off-by-one bug in booking systems: it either blocks the day the guest leaves (so the next guest can't check in that afternoon) or lets two stays overlap by a night.

Shard inventory_night and reservation by listing_id, so every booking transaction is single-shard. Rows for past nights can be archived; rows for future nights beyond the horizon are created by a daily job ("roll the calendar forward one day"). For index design details see database indexing explained.

Concurrent Reservations: Never Double Book

The naive flow, SELECT to check availability, then INSERT the booking, fails when two requests both read "available" before either writes. Three correct techniques:

Technique 1: conditional update across nights (works for hotels and homes)

Java (JDBC)
import java.sql.*;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public final class BookingRepository {

    public long hold(Connection c, long listingId, LocalDate in, LocalDate out,
                     long guestId, long amount, String idemKey) throws SQLException {
        int nights = (int) ChronoUnit.DAYS.between(in, out);
        boolean oldAutoCommit = c.getAutoCommit();
        c.setAutoCommit(false);
        try {
            String sql =
                "UPDATE inventory_night SET reserved_units = reserved_units + 1 " +
                " WHERE listing_id = ? AND night >= ? AND night < ? " +
                "   AND blocked = false " +
                "   AND reserved_units + 1 <= total_units + (total_units * overbook_pct) / 100";
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setLong(1, listingId);
                ps.setObject(2, in);
                ps.setObject(3, out);
                if (ps.executeUpdate() != nights) {          // some night was full
                    c.rollback();
                    throw new NotAvailableException(listingId, in, out);
                }
            }
            long id;
            try (PreparedStatement ps = c.prepareStatement(
                    "INSERT INTO reservation (reservation_id, listing_id, guest_id, " +
                    " check_in, check_out, status, hold_expires_at, total_amount, idempotency_key) " +
                    "VALUES (nextval('reservation_seq'), ?, ?, ?, ?, 'held', " +
                    " now() + interval '10 minutes', ?, ?) RETURNING reservation_id")) {
                ps.setLong(1, listingId);
                ps.setLong(2, guestId);
                ps.setObject(3, in);
                ps.setObject(4, out);
                ps.setLong(5, amount);
                ps.setString(6, idemKey);
                try (ResultSet rs = ps.executeQuery()) {
                    rs.next();
                    id = rs.getLong(1);
                }
            }
            c.commit();
            return id;
        } catch (SQLException e) {
            c.rollback();
            throw e;
        } finally {
            c.setAutoCommit(oldAutoCommit);
        }
    }
}

class NotAvailableException extends RuntimeException {
    NotAvailableException(long listingId, LocalDate in, LocalDate out) {
        super("listing " + listingId + " not available " + in + " to " + out);
    }
}

Why it's safe: in PostgreSQL or MySQL InnoDB, an UPDATE takes a row lock on each matching night. A concurrent transaction updating the same night waits, then re-evaluates the WHERE clause against the committed value. If the night is now full, that row isn't updated, the count comes back short, and the transaction rolls back. Note also that if the calendar has no row for a night (beyond the horizon), the count is short and the booking correctly fails. (The overbooking expression uses integer division; with overbook_pct = 0 it reduces to reserved_units + 1 <= total_units.)

Deadlock note: two transactions locking overlapping date ranges should acquire rows in the same order. A range UPDATE on the primary key (listing_id, night) scans in index order, so they do; if you update nights one by one in application code, sort the dates first. Retry on deadlock errors anyway.

Technique 2: unique constraint per booked night (unique homes)

Insert one row per night into booked_night(listing_id, night) with a primary key. Two overlapping bookings collide on at least one night and the second insert fails with a unique violation. Simple and very robust, but only for single-unit listings.

Technique 3: PostgreSQL exclusion constraint on date ranges

SQL (PostgreSQL)
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE stay (
  reservation_id BIGINT PRIMARY KEY,
  listing_id     BIGINT NOT NULL,
  period         DATERANGE NOT NULL,         -- '[2026-12-24,2026-12-27)'
  status         TEXT NOT NULL,
  EXCLUDE USING gist (listing_id WITH =, period WITH &&)
    WHERE (status IN ('held', 'confirmed'))
);

The database itself rejects any active stay that overlaps another for the same listing. Elegant and impossible to bypass from application code; Postgres-specific and single-unit only.

⚠️
Why not a distributed lock (Redis) per listing?A lock can expire while the holder is paused (GC, network), letting a second booker in. It also doesn't protect against bugs in other code paths that write bookings. Put the invariant in the database. A Redis lock can be an optional optimisation to reduce contention, never the guarantee.

End-to-End Booking Flow

  1. Quote. Compute nightly prices, fees and taxes; return a quote_id valid for ~15 minutes.
  2. Hold. Conditional update of nights + insert reservation with status held and hold_expires_at = now + 10 min (transaction above). Idempotency key prevents duplicate holds on double-click.
  3. Pay. Authorise the payment (card auth or UPI) with the reservation ID as the idempotency key. For many stays, platforms authorise or collect at booking and pay out to the host after check-in.
  4. Confirm. On payment success: UPDATE reservation SET status='confirmed' WHERE id=? AND status='held'. Send confirmations; notify host; push to the hotel's PMS if applicable.
  5. Expire. A sweeper finds holds past expiry and, in one transaction, sets expired and decrements reserved_units for those nights. The conditional status update (WHERE status='held') makes confirm and expire mutually exclusive.
  6. Cancel. Same release transaction plus a refund computed by the cancellation policy (flexible, moderate, strict), handled by the payment service. Payment idempotency and refunds are detailed in payment system design.

Request-to-book listings: create the reservation as requested with a hold of 24 hours (inventory is held during the decision, or not, depending on product policy), authorise payment, and confirm or release on host response or timeout.

This hold-then-confirm shape is the same one you'd use for cinema seats; compare BookMyShow design, where the unit is a specific seat and holds are minutes long.

Search answers: listings near Goa, for 4 guests, with a pool, under ₹12,000/night, available 29 Dec to 2 Jan, ranked by relevance. The first four filters are classic search-engine work; the date filter is what makes this hard, because availability changes with every booking.

Stage 1: retrieve candidates

An Elasticsearch/OpenSearch index with one document per listing: geo point, guests, amenities, property type, base price, rating, and host settings. Geo filtering (bounding box of the map or distance from a point) works as in a proximity service. This narrows 5M listings to a few thousand candidates.

Stage 2: availability filter

Options, in increasing sophistication:

  • Batch check against the availability service: send candidate IDs + dates; it answers from the inventory DB (or a cache of it). Simple, accurate, but a few thousand row-range checks per search.
  • Availability bitmap in the index: store a 365-bit "free nights" bitmap per listing (46 bytes) in the search document or a side cache, updated on every booking/calendar change via CDC. Filtering is a bitwise AND.
  • Precomputed for hotels: store remaining units per night, since a 40-room hotel isn't a binary free/busy.
Python: availability bitmap check
from datetime import date

class AvailabilityBitmap:
    """Bit i = 1 means the listing is free on the night of (start + i days)."""
    def __init__(self, start: date, days: int = 365):
        self.start, self.days = start, days
        self.bits = (1 << days) - 1           # all free

    def _mask(self, check_in: date, check_out: date) -> int:
        a = (check_in - self.start).days
        n = (check_out - check_in).days       # nights; check-out night not needed
        if a < 0 or n <= 0 or a + n > self.days:
            raise ValueError("out of range")
        return ((1 << n) - 1) << a

    def is_free(self, check_in, check_out) -> bool:
        m = self._mask(check_in, check_out)
        return self.bits & m == m

    def book(self, check_in, check_out):
        m = self._mask(check_in, check_out)
        if self.bits & m != m:
            raise RuntimeError("conflict")
        self.bits &= ~m

cal = AvailabilityBitmap(date(2026, 12, 1))
cal.book(date(2026, 12, 24), date(2026, 12, 27))           # nights 24, 25, 26
print(cal.is_free(date(2026, 12, 20), date(2026, 12, 24)))  # True  (checkout on 24th)
print(cal.is_free(date(2026, 12, 26), date(2026, 12, 28)))  # False (26th is taken)
print(cal.is_free(date(2026, 12, 27), date(2026, 12, 31)))  # True

The bitmap in search is a hint, not a guarantee: it lags the database by a second or two, so the booking transaction re-checks. Occasionally a guest will click a listing that just got booked; show "no longer available for these dates" with alternatives.

Stage 3: rank

Rank the available candidates by a learned model: relevance to the query, price competitiveness, reviews, conversion history, host responsiveness, cancellation rate, distance to the map centre, and personalisation. Compute prices for the requested dates (stage 2 can return a cached total) so the price shown in results matches the listing page.

Caching search

Exact query caching has a low hit rate (dates × guests × filters × map bounds). Cache intermediate results instead: candidate sets per geo cell and guest count for a few minutes, and listing summaries by ID.

Pricing: Every Night Has Its Own Price

The price of a stay is a sum over nights plus stay-level adjustments:

ComponentExampleWhere it lives
Base nightly price₹8,000listing.base_price
Weekend / seasonal ruleFri–Sat +20%; 20 Dec–5 Jan +60%Pricing rules table
Per-date override31 Dec: ₹25,000inventory_night.price_override
Length-of-stay discount7+ nights −10%; 28+ nights −25%Listing settings
FeesCleaning ₹1,500 per stay; extra guest ₹800/night above 4Listing settings
Platform service feeA percentage of subtotalPlatform config
TaxesGST on accommodation in India depends on the tariff slab and changes with policy; compute per current rulesTax service

Order of operations matters (do discounts apply before or after cleaning fees? is tax computed per night tariff or on the total?), so put pricing in a single Pricing Service with versioned rules, used by search, listing page, quote and invoice alike. If search and checkout compute prices differently, guests see a different total at checkout, which hurts trust and is a regulatory risk.

Dynamic pricing (suggested prices for hosts, revenue management for hotels) runs offline: models predict demand per area and date from search volume, booking pace and events (a concert in Mumbai, a long weekend), and write suggested overrides. The online system only reads the resulting per-night prices.

Store all money as integers in minor units with a currency code; convert for display with a cached FX rate, but charge in a defined currency.

Overbooking and Channel Synchronisation

Deliberate overbooking (hotels)

If a hotel historically sees 5% no-shows and late cancellations for a given season, selling exactly 40 rooms means about 2 sit empty on average. Revenue management allows selling, say, 42. The schema supports this with overbook_pct per night. The risk is "walking" a guest: if everyone shows up, the hotel relocates someone to a comparable hotel and typically pays for it. Platforms like Airbnb don't overbook because a single home can't be "mostly" available.

Accidental overbooking: multi-channel inventory

A hotel sells the same rooms on its own website, MakeMyTrip, Booking.com, Agoda and walk-ins. The hotel's Property Management System (PMS) or a channel manager is the source of truth and pushes availability to each OTA. Between a booking on one channel and the update reaching the others, a second channel can sell the last room. Mitigations:

  • Push updates immediately on every booking (webhooks/APIs) rather than periodic syncs.
  • Keep a small buffer: don't expose the last 1–2 rooms on third-party channels.
  • Allocation: give each channel a fixed allotment of rooms instead of the shared pool (less flexible, no oversell).
  • Real-time availability check against the PMS at booking time for high-risk dates.

As the OTA in an interview, say: "our inventory is a cached copy of the hotel's; we re-confirm with the hotel's system at booking, and if the hotel rejects, we cancel and refund immediately, with rebooking support."

Architecture

Guest apps / web Host app / Hotel PMS / Channel mgr | | v v +-----------------------------------------------------+ | API Gateway (auth, rate limit) | +----+------------+--------------+-------------+------+ | | | | v v v v +---------+ +----------+ +-----------+ +------------+ | Search | | Listing | | Booking | | Host / | | Service | | Service | | Service | | Calendar | +----+----+ +----+-----+ +-----+-----+ +-----+------+ | | | | v v v v +---------+ +----------+ +-----------------------+ | Elastic | | Listing | | Inventory + Booking | | search | | DB+cache | | DB (SQL, sharded by | | + avail | +----------+ | listing_id) | | bitmaps | +-----------+-----------+ +----^----+ | CDC (Debezium) -> Kafka | v +-------------- indexer ----------+-----------------+ | | +------v------+ +------v-------+ | Pricing svc | | Notification | | (rules, | | email / SMS | | overrides) | +--------------+ +-------------+ ^ +-----------------+------------+ | Payment svc (auth, capture, | | refunds, host payouts) | +------------------------------+
  • Booking Service owns inventory and reservations and is the only writer of those tables.
  • CDC streams inventory changes to the search indexer (availability bitmaps), analytics, and channel sync.
  • Listing data (photos, descriptions) is served from cache/CDN; photos from object storage.
  • Hold sweeper and calendar roller are scheduled jobs.

Consistency Choices

DataConsistencyReason
Inventory, reservationsStrong (single-leader SQL per shard, ACID transactions)No double booking; prefer to reject during failover
Payment stateStrong, idempotentMoney
Search index, availability bitmapsEventual (seconds)Re-checked at booking; huge read volume
Listing details, photos, reviewsEventual (seconds to minutes)Rarely changes, cacheable
Channel/OTA availabilityEventual, with buffers and re-confirmationExternal systems

In CAP terms, the booking path is CP: during a partition between the booking service and the primary database of a shard, bookings for those listings fail fast with a retry message. Search is AP: it keeps serving slightly stale results. Explaining that split clearly is one of the best signals you can give in this interview.

Multi-region: pin each listing's inventory to a home region (where its shard's primary lives). Guests anywhere write to that region for bookings; reads are served locally. Multi-primary writes for the same listing would require conflict resolution, and "two guests both got the villa" isn't a conflict you can merge.

Interview Follow-ups and Mistakes

Follow-ups

  • "Two users book the last room at the same instant." Walk through the row locks: the second transaction blocks, re-evaluates reserved_units + 1 <= total_units, updates fewer rows than nights, rolls back, and gets "not available".
  • "A host blocks dates while a guest is mid-checkout." Blocking is an update to the same rows (blocked = true WHERE reserved_units = 0); either the hold or the block wins atomically. If the hold won, the host sees the dates as booked.
  • "How do you handle a booking that spans the calendar horizon?" Missing nights mean fewer updated rows: the booking fails. Extend the horizon or reject stays beyond it in validation.
  • "How do you scale for New Year's Eve in Goa?" Reads: cache and pre-compute search candidates. Writes: contention is per listing, which is sharded; per-row locks serialise only bookings of the same listing, which is exactly what should be serialised.
  • "How would you build the host's earnings dashboard?" From a ledger of payouts, fed by booking and payment events; read models updated asynchronously.

Common mistakes

  • Check-then-insert without a lock or constraint.
  • Off-by-one on check-out dates.
  • Storing availability only in Elasticsearch and booking against it.
  • A distributed lock as the only protection.
  • Ignoring holds/expiry, so failed payments leave rooms locked forever.
  • Pricing computed differently in search and checkout.

Get the per-night calendar, the atomic hold transaction and the search-then-recheck flow right, and the remaining discussion (pricing, overbooking, channels) becomes an opportunity to show product sense. For more practice on contended inventory, try the flash sale design, where the same "hold, confirm or expire" pattern meets a hundred-thousand-requests-per-second spike.

Frequently Asked Questions

How do booking systems prevent double booking?▾
They make the availability check and the reservation a single atomic database operation. Common techniques are a conditional update on per-night inventory rows that only succeeds if every night still has capacity, a unique constraint on (listing, night) rows, or a PostgreSQL exclusion constraint that rejects overlapping date ranges for the same listing. Application-level check-then-insert without these guarantees will double book under concurrency.
How should availability be stored in a hotel booking database?▾
Store one row per room type per night with total and reserved counts for hotels, or one row per listing per night for unique homes. This makes range checks simple, supports per-night prices and restrictions, and lets a reservation lock exactly the nights it touches. Bookings themselves are stored separately with a check-in and check-out date.
Do hotels really overbook rooms?▾
Many hotels deliberately accept slightly more reservations than rooms, based on historical cancellation and no-show rates, to avoid empty rooms. If everyone shows up, the hotel relocates a guest to a comparable property, often at its own cost. Platforms for unique homes like Airbnb generally cannot overbook because each listing is a single unit.
How does Airbnb search for available listings on specific dates?▾
A typical design first filters listings by location, guests and amenities in a search index, then filters by availability for the requested dates using either availability data stored in the index or a batch call to an availability service, and finally ranks the results. Airbnb's exact production search stack is its own, but this two-stage retrieve-then-filter-and-rank pattern is standard.
Should a booking system use SQL or NoSQL?▾
The booking and inventory core benefits from a relational database because reservations need multi-row atomic updates, constraints and strong consistency. Search, reviews and analytics can use other stores such as Elasticsearch or wide-column databases that are updated asynchronously from the booking database.
What happens if payment fails after a room is reserved?▾
The reservation is held in a pending state with an expiry while payment is attempted. If payment fails or the hold expires, the reservation is cancelled and the nights are released back to inventory in a single transaction. Payment success moves the reservation to confirmed using the reservation ID as the idempotency key.

Design Booking Systems with Confidence

Prepflix's system design course covers reservation systems like Airbnb, BookMyShow and IRCTC with live mock interviews.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.