PrepZone Logo
PrepZone

Hotel Reservation System

Search, booking, payment holds and inventory locks for an Airbnb-style platform.

Read these first

Design StreamStay — StreamHub's travel booking platform (Airbnb-style): search listings by location and dates, book with payment hold, and manage host inventory. Date-range queries, booking conflicts, and payment holds are the core challenges.

Requirements

Functional requirements

  • Search listings by location, check-in/check-out dates, guest count, and price range.
  • View listing details: photos, amenities, reviews, availability calendar.
  • Book a listing: hold dates, collect payment, confirm reservation.
  • Host dashboard: manage listings, set pricing, block dates, view bookings.
  • Guest dashboard: view upcoming and past reservations; cancel with policy.
  • Review system: rate listing after checkout.

Non-functional requirements

  • Scale: 50M users; 5M listings; 500K bookings/day.
  • Search latency: Results under 500 ms p99.
  • Booking consistency: Zero double-bookings — strong consistency on date holds.
  • Payment: Authorise on book, capture 24 h before check-in; idempotent.
  • Availability: 99.9% for search and booking paths.

Out of scope: Dynamic pricing ML, insurance, multi-currency FX, host verification workflow.

Estimation

  • 500K bookings/day × 365 = 182M bookings/year × 500 bytes ≈ 91 GB/year reservation storage.
  • 5M listings × 365 days availability = 1.8B date-slots — bitmap or row-per-date model.
  • Search: 50M users × 5 searches/day = 250M searches/day ≈ 2.9K QPS — Elasticsearch with geo queries.
  • Payment holds: 500K/day ≈ 6 QPS — Stripe auth/capture pattern with idempotency.

Hotel reservation architecture

CLIENT
Guest app
NETWORK
API GatewayEKS ingress
COMPUTE
Booking svcRDS row lock
ANALYTICS
SearchOpenSearch
DATABASE
Hold cacheRedis TTL
INTEGRATION
SNS / SESconfirmation
Search → Redis hold → RDS book → SNS confirmation.

API design

Java
GET  /v1/listings/search:
  query: { lat: 40.71, lng: -74.00, radius_km: 10, check_in: "2026-06-01", check_out: "2026-06-05", guests: 2 }

GET  /v1/listings/{id}
GET  /v1/listings/{id}/availability?month=2026-06

POST /v1/bookings:
  headers: { Idempotency-Key: "uuid" }
  body:
    listing_id: "lst_4420"
    check_in: "2026-06-01"
    check_out: "2026-06-05"
    guests: 2
    payment_method_id: "pm_abc"

GET  /v1/bookings/{id}
DELETE /v1/bookings/{id}              # cancel with policy check
Java
CREATE TABLE listings (
    id          BIGINT PRIMARY KEY,
    host_id     BIGINT NOT NULL,
    title       VARCHAR(256),
    lat         DECIMAL(9,6),
    lng         DECIMAL(9,6),
    price_cents INT NOT NULL,
    max_guests  INT NOT NULL
);

CREATE TABLE availability (
    listing_id  BIGINT NOT NULL,
    date        DATE NOT NULL,
    status      VARCHAR(16) NOT NULL,  -- available, booked, blocked
    booking_id  BIGINT,
    PRIMARY KEY (listing_id, date)
);

CREATE TABLE bookings (
    id           BIGINT PRIMARY KEY,
    listing_id   BIGINT NOT NULL,
    guest_id     BIGINT NOT NULL,
    check_in     DATE NOT NULL,
    check_out    DATE NOT NULL,
    total_cents  INT NOT NULL,
    status       VARCHAR(16) NOT NULL,  -- pending, confirmed, cancelled
    payment_id   VARCHAR(64),
    created_at   TIMESTAMPTZ DEFAULT now()
);

High-level architecture

Services

  • Search service: Elasticsearch with geo_point for location queries; filter by date availability.
  • Listing service: CRUD for listings, photos (S3), amenities; Postgres source of truth.
  • Availability service: Manages date-slot inventory; atomic hold and release.
  • Booking service: Orchestrates hold → payment auth → confirm saga.
  • Payment service: Stripe authorise on book, capture before check-in; idempotency keys.
  • Notification service: Confirmations, reminders, cancellation notices.

Deep dive: availability and double-booking prevention

Java
-- Atomic multi-date hold in one transaction
BEGIN;
UPDATE availability
SET status = 'held', booking_id = 9912
WHERE listing_id = 4420
  AND date BETWEEN '2026-06-01' AND '2026-06-04'
  AND status = 'available';
-- Check rows affected = 4 (number of nights)
-- If < 4 → ROLLBACK (dates unavailable)
COMMIT;
AspectRow-per-date modelBitmap per listing/month
Storage1 row per listing per date1 bitmap per listing per month (31 bits)
Hold operationUPDATE rows with status checkAtomic bitwise AND on bitmap
Query available datesSimple SQL range queryBit operations; harder to query
StreamHub pickRow-per-date — clearer, easier to debugBitmap at 100M+ listings scale
  • Storage

    Row-per-date model1 row per listing per date
    Bitmap per listing/month1 bitmap per listing per month (31 bits)
  • Hold operation

    Row-per-date modelUPDATE rows with status check
    Bitmap per listing/monthAtomic bitwise AND on bitmap
  • Query available dates

    Row-per-date modelSimple SQL range query
    Bitmap per listing/monthBit operations; harder to query
  • StreamHub pick

    Row-per-date modelRow-per-date — clearer, easier to debug
    Bitmap per listing/monthBitmap at 100M+ listings scale

Deep dive: booking saga with payment hold

Booking flow

  • Step 1: Hold dates (atomic UPDATE availability) — TTL 15 min; compensate: release hold.
  • Step 2: Authorise payment (Stripe hold, not capture) with idempotency key — compensate: release auth.
  • Step 3: Confirm booking (status = confirmed); convert hold to booked — compensate: cancel + release dates.
  • Step 4: Schedule payment capture 24 h before check-in via delayed queue job.
  • Step 5: Publish booking.confirmed event for notifications and host dashboard.
Java
def book_listing(req: BookingRequest) -> Booking:
    with db.transaction():
        nights = hold_dates(req.listing_id, req.check_in, req.check_out)
        if not nights:
            raise DatesUnavailable()
        payment = stripe.authorize(req.payment_method, req.total, idempotency_key=req.key)
        booking = db.create_booking(req, payment.id, status="confirmed")
        mark_dates_booked(req.listing_id, nights, booking.id)
    kafka.publish("booking.confirmed", booking)
    schedule_capture(payment.id, req.check_in - timedelta(hours=24))
    return booking

Deep dive: geo search

Elasticsearch geo_distance query on listing coordinates, filtered by listings with all requested dates available. Pre-compute availability bitmaps in the search index as a nightly batch job for fast filtering.

Quick recall

Everything you need if you only revisit this box.

  • Row-per-date availability model; atomic multi-date UPDATE prevents double-booking.
  • Booking saga: hold dates → authorise payment → confirm → schedule capture before check-in.
  • Hold TTL (15 min) releases dates if checkout abandoned.
  • Elasticsearch geo_distance for location search; filter by pre-computed availability.
  • Idempotency-Key on booking POST prevents duplicate reservations on retry.
  • 500K bookings/day ≈ 6 QPS — Postgres handles this; date-range locking is the bottleneck.

Test yourself

Answer these before moving on — recall is what makes it stick.