Skip to content
Unverified — AI-generated content. Help verify this page

Design Booking.com / Airbnb — Hotel & Property Booking Platform

1. Problem Statement & Requirements

Functional Requirements

#RequirementDetails
FR-1Property ListingHosts list properties with photos, amenities, rules, pricing
FR-2Search with FiltersSearch by location, dates, guests, price range, amenities, ratings
FR-3Availability CalendarReal-time availability for each property/room
FR-4Dynamic PricingRates vary by demand, season, day of week, length of stay
FR-5Booking FlowSelect dates, review price, pay, confirm booking
FR-6Reviews & RatingsTwo-way reviews (guest reviews host, host reviews guest)
FR-7Host DashboardManage listings, calendar, pricing, reservations, payouts
FR-8Payment EscrowHold funds until check-in, release to host after stay
FR-9MessagingPre/post booking communication between guest and host
FR-10Cancellation PoliciesFlexible, moderate, strict cancellation with refund rules

Non-Functional Requirements

#RequirementTarget
NFR-1Availability99.99% uptime
NFR-2LatencySearch results < 500ms
NFR-3ConsistencyStrong consistency for bookings (no double-booking)
NFR-4Throughput1M+ searches/min, 10K+ bookings/min
NFR-5Scale7M+ active listings, 150M+ users
NFR-6GlobalMulti-currency, multi-language, tax compliance

2. Back-of-Envelope Estimation

User Scale

Total Users=150MActive Listings=7MDAU=20M

Search Volume

Searches/Day=20M×10=200MSearch QPS=200M86,4002,315 req/sPeak Search QPS2,315×5=11,575 req/s

Booking Volume

Daily Bookings=2MBooking QPS=2M86,40023 req/sPeak Booking QPS23×10=230 req/s

Storage Estimation

Listings:

Listing Metadata=7M×10 KB=70 GBListing Photos=7M×20 photos×2 MB=280 TB

Availability Calendar:

Calendar Entries=7M×365 days=2.55B rowsCalendar Storage=2.55B×100 B=255 GB

Reviews:

Total Reviews=500M×500 B avg=250 GB

3. High-Level Design

Architecture Diagram

API Design

typescript
// Search APIs
GET /api/v1/search?location={city}&check_in={date}&check_out={date}
    &guests={n}&min_price={n}&max_price={n}&amenities={list}
    &property_type={type}&rating_min={n}&sort={relevance|price|rating}
    &page={n}&limit=20

// Listing APIs
POST   /api/v1/listings
GET    /api/v1/listings/{listingId}
PUT    /api/v1/listings/{listingId}
POST   /api/v1/listings/{listingId}/photos
GET    /api/v1/listings/{listingId}/availability?from={date}&to={date}
PUT    /api/v1/listings/{listingId}/availability
       // Body: { dates: [{ date, available, price }] }

// Booking APIs
POST /api/v1/bookings
     // Body: { listingId, checkIn, checkOut, guests, paymentMethodId }
GET  /api/v1/bookings/{bookingId}
POST /api/v1/bookings/{bookingId}/cancel
POST /api/v1/bookings/{bookingId}/confirm  // Host confirms (instant or manual)

// Payment APIs
POST /api/v1/payments/intent
     // Body: { bookingId, amount, currency }
GET  /api/v1/hosts/{hostId}/payouts

// Review APIs
POST /api/v1/bookings/{bookingId}/reviews
     // Body: { rating, text, categories: { cleanliness, accuracy, ... } }
GET  /api/v1/listings/{listingId}/reviews?page={n}

4. Database Schema

Listings Table (PostgreSQL)

sql
CREATE TABLE listings (
    listing_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    host_id         UUID NOT NULL REFERENCES users(user_id),
    title           VARCHAR(200) NOT NULL,
    description     TEXT,
    property_type   VARCHAR(30) NOT NULL, -- apartment, house, villa, hotel_room
    room_type       VARCHAR(30) NOT NULL, -- entire_place, private_room, shared_room
    max_guests      SMALLINT NOT NULL,
    bedrooms        SMALLINT DEFAULT 1,
    beds            SMALLINT DEFAULT 1,
    bathrooms       DECIMAL(3,1) DEFAULT 1,
    latitude        DECIMAL(9,6) NOT NULL,
    longitude       DECIMAL(9,6) NOT NULL,
    city            VARCHAR(100),
    country         CHAR(2),
    amenities       TEXT[], -- ['wifi', 'pool', 'parking', 'kitchen', ...]
    house_rules     JSONB,
    cancellation_policy VARCHAR(20) DEFAULT 'moderate', -- flexible, moderate, strict
    base_price      DECIMAL(10,2) NOT NULL,
    currency        CHAR(3) DEFAULT 'USD',
    cleaning_fee    DECIMAL(10,2) DEFAULT 0,
    service_fee_pct DECIMAL(4,2) DEFAULT 14.0, -- Airbnb takes ~14%
    min_nights      SMALLINT DEFAULT 1,
    max_nights      SMALLINT DEFAULT 365,
    instant_book    BOOLEAN DEFAULT FALSE,
    status          VARCHAR(20) DEFAULT 'active', -- active, paused, deleted
    avg_rating      DECIMAL(3,2) DEFAULT 0,
    review_count    INT DEFAULT 0,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_listings_host ON listings(host_id);
CREATE INDEX idx_listings_location ON listings USING GIST (
    point(longitude, latitude)
);
CREATE INDEX idx_listings_city ON listings(city, status);
CREATE INDEX idx_listings_price ON listings(base_price);
CREATE INDEX idx_listings_rating ON listings(avg_rating DESC);

Availability Calendar Table

sql
CREATE TABLE availability_calendar (
    listing_id      UUID NOT NULL REFERENCES listings(listing_id),
    cal_date        DATE NOT NULL,
    available       BOOLEAN DEFAULT TRUE,
    price           DECIMAL(10,2),       -- Override price for this date
    min_nights      SMALLINT,            -- Override min nights
    booking_id      UUID REFERENCES bookings(booking_id),
    PRIMARY KEY (listing_id, cal_date)
);

-- Partition by month for performance
CREATE TABLE availability_calendar_2026_01 PARTITION OF availability_calendar
    FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE availability_calendar_2026_02 PARTITION OF availability_calendar
    FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
-- ... etc

CREATE INDEX idx_avail_date ON availability_calendar(cal_date, available);

Bookings Table

sql
CREATE TABLE bookings (
    booking_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    listing_id      UUID NOT NULL REFERENCES listings(listing_id),
    guest_id        UUID NOT NULL REFERENCES users(user_id),
    host_id         UUID NOT NULL REFERENCES users(user_id),
    check_in        DATE NOT NULL,
    check_out       DATE NOT NULL,
    guests          SMALLINT NOT NULL,
    nights          SMALLINT NOT NULL,
    nightly_rate    DECIMAL(10,2) NOT NULL,
    cleaning_fee    DECIMAL(10,2) DEFAULT 0,
    service_fee     DECIMAL(10,2) NOT NULL,
    taxes           DECIMAL(10,2) DEFAULT 0,
    total_price     DECIMAL(10,2) NOT NULL,
    currency        CHAR(3) DEFAULT 'USD',
    status          VARCHAR(20) DEFAULT 'pending',
    -- pending, confirmed, active, completed, cancelled, declined
    payment_intent_id VARCHAR(100),
    host_payout_id  VARCHAR(100),
    cancelled_by    UUID,
    cancellation_reason TEXT,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW(),
    CONSTRAINT valid_dates CHECK (check_out > check_in)
);

CREATE INDEX idx_bookings_listing ON bookings(listing_id, check_in, check_out);
CREATE INDEX idx_bookings_guest ON bookings(guest_id, status);
CREATE INDEX idx_bookings_host ON bookings(host_id, status);
CREATE INDEX idx_bookings_status ON bookings(status) WHERE status IN ('pending', 'confirmed', 'active');

Reviews Table (Cassandra for scale)

sql
CREATE TABLE reviews (
    listing_id      UUID,
    review_id       TIMEUUID,
    booking_id      UUID,
    reviewer_id     UUID,
    reviewer_type   TEXT, -- 'guest' or 'host'
    overall_rating  SMALLINT, -- 1-5
    cleanliness     SMALLINT,
    accuracy        SMALLINT,
    communication   SMALLINT,
    location_rating SMALLINT,
    check_in_rating SMALLINT,
    value_rating    SMALLINT,
    text            TEXT,
    response_text   TEXT, -- Host's response
    created_at      TIMESTAMP,
    PRIMARY KEY ((listing_id), review_id)
) WITH CLUSTERING ORDER BY (review_id DESC);

Pricing Rules Table

sql
CREATE TABLE pricing_rules (
    rule_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    listing_id      UUID NOT NULL REFERENCES listings(listing_id),
    rule_type       VARCHAR(30) NOT NULL,
    -- weekend, weekly_discount, monthly_discount, seasonal, last_minute, early_bird
    day_of_week     SMALLINT[],          -- 0=Sun..6=Sat (for weekend pricing)
    start_date      DATE,                -- For seasonal pricing
    end_date        DATE,
    adjustment_type VARCHAR(10) NOT NULL, -- 'percentage' or 'fixed'
    adjustment_value DECIMAL(10,2) NOT NULL, -- e.g., -10 for 10% discount
    min_nights      SMALLINT,            -- For length-of-stay discounts
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_pricing_listing ON pricing_rules(listing_id);

5. Detailed Component Design

5.1 Search with Filters

The search system must handle complex multi-dimensional queries efficiently.

Elasticsearch mapping:

typescript
const listingMapping = {
  properties: {
    listing_id: { type: 'keyword' },
    title: { type: 'text', analyzer: 'standard' },
    description: { type: 'text', analyzer: 'standard' },
    property_type: { type: 'keyword' },
    room_type: { type: 'keyword' },
    location: { type: 'geo_point' },
    city: { type: 'keyword' },
    country: { type: 'keyword' },
    max_guests: { type: 'integer' },
    bedrooms: { type: 'integer' },
    bathrooms: { type: 'float' },
    amenities: { type: 'keyword' }, // Array of keywords
    base_price: { type: 'float' },
    avg_rating: { type: 'float' },
    review_count: { type: 'integer' },
    instant_book: { type: 'boolean' },
    status: { type: 'keyword' },
    host_is_superhost: { type: 'boolean' },
  },
};
typescript
class SearchService {
  async search(params: SearchParams): Promise<SearchResults> {
    // Step 1: Build Elasticsearch query
    const esQuery = this.buildQuery(params);

    // Step 2: Execute search (returns candidate listings)
    const candidates = await this.elasticsearch.search({
      index: 'listings',
      body: esQuery,
      size: params.limit * 3, // Over-fetch for availability filtering
    });

    // Step 3: Check availability for the date range
    const listingIds = candidates.hits.hits.map(h => h._id);
    const availability = await this.availabilityService.checkBulkAvailability(
      listingIds, params.checkIn, params.checkOut
    );

    const availableListings = candidates.hits.hits.filter(
      h => availability.get(h._id)?.available
    );

    // Step 4: Calculate prices for the date range
    const priced = await Promise.all(
      availableListings.map(async (listing) => {
        const price = await this.pricingService.calculateTotalPrice(
          listing._id, params.checkIn, params.checkOut, params.guests
        );
        return { ...listing._source, totalPrice: price };
      })
    );

    // Step 5: Rank and paginate
    const ranked = this.rank(priced, params.sort);
    const page = ranked.slice(params.offset, params.offset + params.limit);

    return {
      results: page,
      total: availability.size,
      page: params.page,
      hasMore: params.offset + params.limit < ranked.length,
    };
  }

  private buildQuery(params: SearchParams): object {
    const must: object[] = [];
    const filter: object[] = [];

    // Location filter (geo bounding box or radius)
    if (params.lat && params.lng) {
      filter.push({
        geo_distance: {
          distance: `${params.radiusKm ?? 20}km`,
          location: { lat: params.lat, lon: params.lng },
        },
      });
    } else if (params.location) {
      filter.push({ term: { city: params.location } });
    }

    // Guest capacity
    filter.push({ range: { max_guests: { gte: params.guests } } });

    // Price range
    if (params.minPrice || params.maxPrice) {
      const priceRange: Record<string, number> = {};
      if (params.minPrice) priceRange.gte = params.minPrice;
      if (params.maxPrice) priceRange.lte = params.maxPrice;
      filter.push({ range: { base_price: priceRange } });
    }

    // Amenities
    if (params.amenities?.length) {
      for (const amenity of params.amenities) {
        filter.push({ term: { amenities: amenity } });
      }
    }

    // Property type
    if (params.propertyType) {
      filter.push({ term: { property_type: params.propertyType } });
    }

    // Minimum rating
    if (params.ratingMin) {
      filter.push({ range: { avg_rating: { gte: params.ratingMin } } });
    }

    // Text search
    if (params.query) {
      must.push({
        multi_match: {
          query: params.query,
          fields: ['title^3', 'description', 'city^2'],
        },
      });
    }

    // Only active listings
    filter.push({ term: { status: 'active' } });

    return {
      query: {
        bool: { must, filter },
      },
      sort: this.getSortClause(params.sort),
    };
  }
}

5.2 Availability Calendar

typescript
class AvailabilityService {
  async checkAvailability(
    listingId: string,
    checkIn: Date,
    checkOut: Date
  ): Promise<AvailabilityResult> {
    // Try cache first
    const cacheKey = `avail:${listingId}:${this.formatMonth(checkIn)}`;
    const cached = await this.redis.get(cacheKey);

    if (cached) {
      const calendar = JSON.parse(cached);
      return this.checkDatesInCalendar(calendar, checkIn, checkOut);
    }

    // Cache miss: query database
    const calendar = await this.db.query(`
      SELECT cal_date, available, price, min_nights
      FROM availability_calendar
      WHERE listing_id = $1
        AND cal_date >= $2
        AND cal_date < $3
      ORDER BY cal_date
    `, [listingId, checkIn, checkOut]);

    // All dates must be available
    const dates = calendar.rows;
    const allAvailable = dates.length === this.daysBetween(checkIn, checkOut)
      && dates.every(d => d.available);

    // Check minimum nights requirement
    const listing = await this.getListing(listingId);
    const nights = this.daysBetween(checkIn, checkOut);
    const minNights = dates[0]?.min_nights ?? listing.min_nights;

    return {
      available: allAvailable && nights >= minNights,
      dates: dates.map(d => ({
        date: d.cal_date,
        available: d.available,
        price: d.price,
      })),
      minNights,
      nights,
    };
  }

  async blockDates(
    listingId: string,
    checkIn: Date,
    checkOut: Date,
    bookingId: string
  ): Promise<boolean> {
    // Use advisory lock to prevent race conditions
    return await this.db.transaction(async (tx) => {
      // Lock the calendar rows
      const rows = await tx.query(`
        SELECT cal_date, available
        FROM availability_calendar
        WHERE listing_id = $1
          AND cal_date >= $2
          AND cal_date < $3
        FOR UPDATE
      `, [listingId, checkIn, checkOut]);

      // Verify all dates still available
      if (!rows.rows.every(r => r.available)) {
        return false; // Dates no longer available
      }

      // Block the dates
      await tx.query(`
        UPDATE availability_calendar
        SET available = FALSE, booking_id = $3
        WHERE listing_id = $1
          AND cal_date >= $2
          AND cal_date < $3
      `, [listingId, checkIn, checkOut, bookingId]);

      // Invalidate cache
      await this.invalidateCache(listingId, checkIn, checkOut);

      return true;
    });
  }

  // Bulk availability check for search results
  async checkBulkAvailability(
    listingIds: string[],
    checkIn: Date,
    checkOut: Date
  ): Promise<Map<string, { available: boolean }>> {
    const nights = this.daysBetween(checkIn, checkOut);
    const results = new Map<string, { available: boolean }>();

    // Query all at once
    const rows = await this.db.query(`
      SELECT listing_id, COUNT(*) as available_nights
      FROM availability_calendar
      WHERE listing_id = ANY($1)
        AND cal_date >= $2
        AND cal_date < $3
        AND available = TRUE
      GROUP BY listing_id
    `, [listingIds, checkIn, checkOut]);

    const availMap = new Map(rows.rows.map(r => [r.listing_id, r.available_nights]));

    for (const id of listingIds) {
      results.set(id, {
        available: (availMap.get(id) ?? 0) === nights,
      });
    }

    return results;
  }
}

5.3 Dynamic Pricing Engine

typescript
class PricingEngine {
  async calculateTotalPrice(
    listingId: string,
    checkIn: Date,
    checkOut: Date,
    guests: number
  ): Promise<PriceBreakdown> {
    const listing = await this.getListing(listingId);
    const rules = await this.getPricingRules(listingId);
    const nights = this.daysBetween(checkIn, checkOut);

    let nightlyPrices: NightlyPrice[] = [];
    let currentDate = new Date(checkIn);

    while (currentDate < checkOut) {
      let price = listing.base_price;

      // Apply date-specific override from calendar
      const calendarPrice = await this.getCalendarPrice(listingId, currentDate);
      if (calendarPrice !== null) {
        price = calendarPrice;
      }

      // Apply day-of-week adjustment
      const dayOfWeek = currentDate.getDay();
      const weekendRule = rules.find(r =>
        r.rule_type === 'weekend' && r.day_of_week?.includes(dayOfWeek)
      );
      if (weekendRule) {
        price = this.applyAdjustment(price, weekendRule);
      }

      // Apply seasonal adjustment
      const seasonalRule = rules.find(r =>
        r.rule_type === 'seasonal'
        && r.start_date! <= currentDate
        && r.end_date! >= currentDate
      );
      if (seasonalRule) {
        price = this.applyAdjustment(price, seasonalRule);
      }

      // Apply Smart Pricing (demand-based)
      if (listing.smart_pricing_enabled) {
        const demandMultiplier = await this.getSmartPricingMultiplier(listing, currentDate);
        price = Math.round(price * demandMultiplier * 100) / 100;

        // Respect host's min/max bounds
        price = Math.max(listing.smart_price_min ?? price * 0.5, price);
        price = Math.min(listing.smart_price_max ?? price * 2.0, price);
      }

      nightlyPrices.push({ date: new Date(currentDate), price });
      currentDate.setDate(currentDate.getDate() + 1);
    }

    // Apply length-of-stay discounts
    let subtotal = nightlyPrices.reduce((sum, np) => sum + np.price, 0);
    let discount = 0;

    const weeklyRule = rules.find(r => r.rule_type === 'weekly_discount' && nights >= 7);
    const monthlyRule = rules.find(r => r.rule_type === 'monthly_discount' && nights >= 28);

    if (monthlyRule) {
      discount = subtotal * Math.abs(monthlyRule.adjustment_value) / 100;
    } else if (weeklyRule) {
      discount = subtotal * Math.abs(weeklyRule.adjustment_value) / 100;
    }

    const nightlySubtotal = subtotal - discount;
    const cleaningFee = listing.cleaning_fee;
    const serviceFee = Math.round(nightlySubtotal * listing.service_fee_pct / 100 * 100) / 100;
    const taxes = await this.calculateTaxes(listing, nightlySubtotal + cleaningFee);

    return {
      nightlyPrices,
      nights,
      avgNightlyRate: Math.round(nightlySubtotal / nights * 100) / 100,
      subtotal: nightlySubtotal,
      cleaningFee,
      serviceFee,
      taxes,
      total: Math.round((nightlySubtotal + cleaningFee + serviceFee + taxes) * 100) / 100,
      currency: listing.currency,
      discount: discount > 0 ? { type: monthlyRule ? 'monthly' : 'weekly', amount: discount } : undefined,
    };
  }

  // AI-powered demand-based pricing
  private async getSmartPricingMultiplier(listing: Listing, date: Date): Promise<number> {
    // Factors: area demand, day of week, holidays, events, competitor prices
    const [demand, competitors, events] = await Promise.all([
      this.getDemandScore(listing.city, date),
      this.getCompetitorPrices(listing, date),
      this.getLocalEvents(listing.city, date),
    ]);

    let multiplier = 1.0;

    // High demand increases price
    if (demand > 0.8) multiplier += 0.3;
    else if (demand > 0.6) multiplier += 0.15;
    else if (demand < 0.3) multiplier -= 0.15;

    // Local events (concerts, conferences) increase demand
    if (events.length > 0) multiplier += 0.2;

    // Competitor pricing adjustment
    if (competitors.avgPrice > listing.base_price * 1.3) {
      multiplier += 0.1;
    }

    return Math.max(0.5, Math.min(2.0, multiplier));
  }
}

5.4 Booking Flow

typescript
class BookingService {
  async createBooking(request: BookingRequest): Promise<Booking> {
    return await this.db.transaction(async (tx) => {
      // Step 1: Lock and verify availability
      const available = await this.availabilityService.blockDates(
        request.listingId,
        request.checkIn,
        request.checkOut,
        'pending' // Temporary block
      );

      if (!available) {
        throw new DatesUnavailableError('Selected dates are no longer available');
      }

      // Step 2: Calculate price
      const price = await this.pricingService.calculateTotalPrice(
        request.listingId, request.checkIn, request.checkOut, request.guests
      );

      // Step 3: Create payment intent (hold funds)
      const paymentIntent = await this.paymentService.createPaymentIntent({
        amount: price.total,
        currency: price.currency,
        customerId: request.guestId,
        metadata: { listingId: request.listingId },
      });

      // Step 4: Create booking record
      const listing = await this.getListing(request.listingId);
      const booking = await tx.query(`
        INSERT INTO bookings (
          listing_id, guest_id, host_id, check_in, check_out,
          guests, nights, nightly_rate, cleaning_fee, service_fee,
          taxes, total_price, currency, status, payment_intent_id
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
        RETURNING *
      `, [
        request.listingId, request.guestId, listing.host_id,
        request.checkIn, request.checkOut, request.guests,
        price.nights, price.avgNightlyRate, price.cleaningFee,
        price.serviceFee, price.taxes, price.total, price.currency,
        listing.instant_book ? 'confirmed' : 'pending_approval',
        paymentIntent.id,
      ]);

      // Step 5: Finalize date blocks with booking ID
      await this.availabilityService.blockDates(
        request.listingId,
        request.checkIn,
        request.checkOut,
        booking.rows[0].booking_id
      );

      // Step 6: Publish events
      await this.kafka.publish('booking.created', booking.rows[0]);

      // Step 7: Schedule expiry if pending approval (host has 24h to respond)
      if (!listing.instant_book) {
        await this.scheduler.schedule(
          'booking.auto_decline',
          { bookingId: booking.rows[0].booking_id },
          new Date(Date.now() + 24 * 60 * 60 * 1000)
        );
      }

      return booking.rows[0];
    });
  }
}

5.5 Payment Escrow

typescript
class PaymentEscrowService {
  async processBookingPayment(bookingId: string): Promise<void> {
    const booking = await this.getBooking(bookingId);

    // Charge guest's card and hold in platform escrow
    const charge = await this.stripe.charges.create({
      amount: Math.round(booking.total_price * 100), // cents
      currency: booking.currency,
      customer: booking.guest_stripe_id,
      transfer_group: bookingId,
      metadata: { bookingId },
    });

    await this.updateBooking(bookingId, {
      payment_status: 'captured',
      charge_id: charge.id,
    });

    // Schedule payout to host: 24 hours after check-in
    const payoutDate = new Date(booking.check_in);
    payoutDate.setDate(payoutDate.getDate() + 1);

    await this.scheduler.schedule('release_payout', { bookingId }, payoutDate);
  }

  async releasePayout(bookingId: string): Promise<void> {
    const booking = await this.getBooking(bookingId);
    if (booking.status === 'cancelled') return;

    // Calculate host payout (total - platform commission)
    const platformFee = booking.service_fee;
    const hostPayout = booking.total_price - platformFee;

    // Transfer to host's connected Stripe account
    const transfer = await this.stripe.transfers.create({
      amount: Math.round(hostPayout * 100),
      currency: booking.currency,
      destination: booking.host_stripe_account_id,
      transfer_group: bookingId,
    });

    await this.updateBooking(bookingId, {
      host_payout_id: transfer.id,
      payout_status: 'completed',
    });
  }

  async processCancellationRefund(bookingId: string): Promise<void> {
    const booking = await this.getBooking(bookingId);
    const listing = await this.getListing(booking.listing_id);
    const daysUntilCheckIn = this.daysBetween(new Date(), booking.check_in);

    // Calculate refund based on cancellation policy
    let refundPercentage: number;

    switch (listing.cancellation_policy) {
      case 'flexible':
        refundPercentage = daysUntilCheckIn >= 1 ? 100 : 0;
        break;
      case 'moderate':
        refundPercentage = daysUntilCheckIn >= 5 ? 100 :
                           daysUntilCheckIn >= 1 ? 50 : 0;
        break;
      case 'strict':
        refundPercentage = daysUntilCheckIn >= 14 ? 100 :
                           daysUntilCheckIn >= 7 ? 50 : 0;
        break;
      default:
        refundPercentage = 0;
    }

    if (refundPercentage > 0) {
      const refundAmount = booking.total_price * refundPercentage / 100;
      await this.stripe.refunds.create({
        charge: booking.charge_id,
        amount: Math.round(refundAmount * 100),
      });
    }
  }
}

5.6 Reviews System

typescript
class ReviewService {
  // Both guest and host have a 14-day window to leave a review after checkout
  async submitReview(bookingId: string, reviewerId: string, review: ReviewInput): Promise<void> {
    const booking = await this.getBooking(bookingId);

    // Validate: reviewer must be guest or host of this booking
    const isGuest = booking.guest_id === reviewerId;
    const isHost = booking.host_id === reviewerId;
    if (!isGuest && !isHost) throw new UnauthorizedError();

    // Check review window (14 days after checkout)
    const daysSinceCheckout = this.daysBetween(booking.check_out, new Date());
    if (daysSinceCheckout > 14) throw new ReviewWindowClosedError();

    // Check for existing review
    const existing = await this.getReview(bookingId, reviewerId);
    if (existing) throw new DuplicateReviewError();

    // Store review
    await this.cassandra.execute(`
      INSERT INTO reviews (
        listing_id, review_id, booking_id, reviewer_id, reviewer_type,
        overall_rating, cleanliness, accuracy, communication,
        location_rating, check_in_rating, value_rating, text, created_at
      )
      VALUES (?, now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, toTimestamp(now()))
    `, [
      booking.listing_id, bookingId, reviewerId,
      isGuest ? 'guest' : 'host',
      review.overallRating, review.cleanliness, review.accuracy,
      review.communication, review.location, review.checkIn,
      review.value, review.text,
    ]);

    // Reviews are hidden until both parties submit (or 14 days pass)
    // This prevents retaliatory reviews
    const otherReview = await this.getReview(
      bookingId, isGuest ? booking.host_id : booking.guest_id
    );

    if (otherReview) {
      // Both reviews submitted - make both visible
      await this.publishReviews(bookingId);
    }

    // Update listing's average rating
    if (isGuest) {
      await this.updateListingRating(booking.listing_id);
    }
  }

  private async updateListingRating(listingId: string): Promise<void> {
    const stats = await this.cassandra.execute(`
      SELECT AVG(overall_rating) as avg_rating, COUNT(*) as count
      FROM reviews
      WHERE listing_id = ?
    `, [listingId]);

    await this.db.query(`
      UPDATE listings SET avg_rating = $1, review_count = $2, updated_at = NOW()
      WHERE listing_id = $3
    `, [stats.rows[0].avg_rating, stats.rows[0].count, listingId]);

    // Update search index
    await this.searchService.updateListing(listingId, {
      avg_rating: stats.rows[0].avg_rating,
      review_count: parseInt(stats.rows[0].count),
    });
  }
}

6. Scaling & Bottlenecks

What Breaks First

ComponentBottleneckSolution
Search11K+ QPS with complex geo+filter queriesElasticsearch cluster with geo-sharding
Availability Check2.55B calendar rows queried per searchRedis cache, date-range partitioning
Booking ConcurrencyMultiple guests booking same datesPessimistic locking (FOR UPDATE)
Photo Storage280 TB, high read throughputCDN with image optimization (WebP, responsive)
Price CalculationCompute per listing per search resultPre-compute and cache nightly prices
Review AggregationCount/average over millions of reviewsMaterialized counters on listing table

Search Performance Optimization

typescript
class SearchOptimizer {
  // Pre-warm cache with popular search queries
  async prewarmCache(): Promise<void> {
    const popularSearches = await this.getTopSearchQueries(1000);

    for (const search of popularSearches) {
      const results = await this.searchService.search(search.params);
      await this.redis.set(
        `search_cache:${this.hashParams(search.params)}`,
        JSON.stringify(results),
        'EX', 300 // 5 min cache
      );
    }
  }

  // Shard Elasticsearch by geography
  getShardRoutingKey(listing: Listing): string {
    // Route to region-specific shard for better locality
    return listing.country; // EU listings on EU shard, US on US shard
  }
}

7. Trade-offs & Alternatives

Availability Storage

ApproachProCon
Date-per-row (current)Simple queries, easy to understand2.55B rows, large table
Bitmap (1 bit per day)Compact storageComplex queries
Range-based (start/end)Fewer rows for long blocksOverlapping ranges complex

Booking Consistency Model

ApproachProCon
Pessimistic Lock (current)No double booking guaranteedLock contention under load
Optimistic Lock (version)Better throughputRetry on conflict
Distributed Lock (Redis)Works across servicesRedis SPOF
Saga PatternLoosely coupled microservicesCompensating transactions

Search Architecture

ApproachProCon
Elasticsearch (current)Great full-text + geoStale data, eventual consistency
PostgreSQL + PostGISStrong consistencySlower at scale
SolrEnterprise featuresLess modern ecosystem
Custom (Airbnb)Optimized for use caseHuge engineering investment

8. Advanced Topics

8.1 Multi-Currency Support

Display Price=Base Price×Exchange Rate×(1+Currency Margin)
typescript
class CurrencyService {
  private rates: Map<string, number> = new Map();

  async convertPrice(amount: number, from: string, to: string): Promise<number> {
    if (from === to) return amount;

    const rate = await this.getRate(from, to);
    return Math.round(amount * rate * 100) / 100;
  }

  // Cache exchange rates, refresh every hour
  async refreshRates(): Promise<void> {
    const rates = await this.exchangeRateAPI.getLatestRates('USD');
    for (const [currency, rate] of Object.entries(rates)) {
      this.rates.set(`USD_${currency}`, rate as number);
    }
    // Store in Redis for all service instances
    await this.redis.set('exchange_rates', JSON.stringify(Object.fromEntries(this.rates)), 'EX', 3600);
  }
}

8.2 iCal Synchronization

Hosts often list on multiple platforms (Airbnb, Booking.com, VRBO). Calendar sync prevents double bookings.

8.3 Trust & Safety

Verification systems for both hosts and guests: government ID verification, phone verification, social media linking, review history, and response rate tracking.


9. Interview Tips

Key Points to Emphasize

  1. Double-booking prevention is the hardest problem — Use pessimistic locking on availability calendar.
  2. Search must be fast despite availability checks — Cache aggressively, pre-compute prices.
  3. Payment escrow is essential for marketplace trust — Explain the timeline from charge to payout.
  4. Dynamic pricing drives revenue — Demand-based pricing with host-set bounds.
  5. Two-sided marketplace dynamics — Both guest and host experience matter equally.

Common Mistakes

  • Not addressing the double-booking problem with proper concurrency control.
  • Ignoring the complexity of multi-currency, multi-tax, multi-language support.
  • Treating search as a simple database query — it requires a dedicated search engine.
  • Forgetting the host side of the marketplace (calendar management, payouts, reviews).
  • Not discussing cancellation policies and their impact on refund flows.

Follow-Up Questions to Expect

  • How would you handle a property listed on both Airbnb and Booking.com? (iCal sync, channel manager integration.)
  • How would you detect and prevent fraudulent listings?
  • How would you implement "Flexible Dates" search (show cheapest dates in a range)?
  • How would you handle group bookings across multiple listings?

Time Allocation in 45-min Interview

PhaseTimeFocus
Requirements5 minClarify scope: hotel vs. vacation rental, instant vs. request
High-Level Design8 minArchitecture, key services, data stores
Deep Dive: Search8 minElasticsearch, availability check integration
Deep Dive: Booking10 minConcurrency control, payment escrow, cancellation
Deep Dive: Pricing7 minDynamic pricing, calendar management
Scaling5 minCaching strategies, search optimization
Q&A2 minMulti-currency, trust & safety

"What I cannot create, I do not understand." — Richard Feynman