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
API design
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
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
-- 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;
| Aspect | Row-per-date model | Bitmap per listing/month |
|---|---|---|
| Storage | 1 row per listing per date | 1 bitmap per listing per month (31 bits) |
| Hold operation | UPDATE rows with status check | Atomic bitwise AND on bitmap |
| Query available dates | Simple SQL range query | Bit operations; harder to query |
| StreamHub pick | Row-per-date — clearer, easier to debug | Bitmap at 100M+ listings scale |
Storage
Row-per-date model1 row per listing per dateBitmap per listing/month1 bitmap per listing per month (31 bits)Hold operation
Row-per-date modelUPDATE rows with status checkBitmap per listing/monthAtomic bitwise AND on bitmapQuery available dates
Row-per-date modelSimple SQL range queryBitmap per listing/monthBit operations; harder to queryStreamHub pick
Row-per-date modelRow-per-date — clearer, easier to debugBitmap 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.confirmedevent for notifications and host dashboard.
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.