Requirements
Functional: search available rooms by date range and city, view room details and pricing, book a room (atomic reservation), cancel with refund policy, manage check-in/check-out, support multiple room types per property, dynamic pricing (rates change by date/season).
Non-functional: no double-booking (atomic reservation), search results show real-time availability, idempotent booking (no duplicate charges on retry), 99.99% uptime during peak travel periods.
Core Entities
from enum import Enum
from dataclasses import dataclass, field
from typing import Optional, List, Dict
from datetime import date, datetime
class RoomType(Enum):
STANDARD = "STANDARD"
DELUXE = "DELUXE"
SUITE = "SUITE"
PENTHOUSE = "PENTHOUSE"
class BookingStatus(Enum):
PENDING = "PENDING" # payment not yet confirmed
CONFIRMED = "CONFIRMED"
CHECKED_IN = "CHECKED_IN"
CHECKED_OUT = "CHECKED_OUT"
CANCELLED = "CANCELLED"
NO_SHOW = "NO_SHOW"
@dataclass
class Room:
room_id: str
hotel_id: str
room_number: str
room_type: RoomType
max_occupancy: int
amenities: List[str]
base_price_cents: int # nightly base rate
is_active: bool = True
@dataclass
class RoomAvailability:
room_id: str
date: date
is_available: bool
price_cents: int # may differ from base (dynamic pricing)
@dataclass
class Booking:
booking_id: str
user_id: str
room_id: str
hotel_id: str
check_in: date
check_out: date
status: BookingStatus
total_cents: int
payment_intent_id: str
idempotency_key: str
created_at: datetime
cancelled_at: Optional[datetime] = None
refund_cents: Optional[int] = None
@dataclass
class Hotel:
hotel_id: str
name: str
city: str
country: str
star_rating: int
latitude: float
longitude: float
rooms: List[Room] = field(default_factory=list)
Availability Check
class AvailabilityService:
def search_available_rooms(self, hotel_id: str, check_in: date,
check_out: date, guests: int) -> List[dict]:
"""Find rooms available for the entire requested stay."""
# All dates in the requested range
nights = (check_out - check_in).days
dates = [check_in + timedelta(days=i) for i in range(nights)]
# Rooms not booked for any date in range
booked_room_ids = db.query(
"""SELECT DISTINCT room_id FROM bookings
WHERE hotel_id = %s
AND status IN ('CONFIRMED', 'CHECKED_IN')
AND check_in %s""",
[hotel_id, check_out, check_in]
)
all_rooms = db.query(
"SELECT * FROM rooms WHERE hotel_id = %s AND is_active = TRUE "
"AND max_occupancy >= %s",
[hotel_id, guests]
)
available = [r for r in all_rooms if r.room_id not in booked_room_ids]
# Attach pricing per date (dynamic pricing)
result = []
for room in available:
prices = self._get_prices(room.room_id, dates)
result.append({
'room': room,
'nightly_prices': prices,
'total_cents': sum(prices.values()),
})
return result
def _get_prices(self, room_id: str, dates: list) -> Dict[date, int]:
"""Fetch dynamic pricing — falls back to base rate if no override."""
overrides = {row.date: row.price_cents
for row in db.query(
"SELECT date, price_cents FROM room_availability "
"WHERE room_id = %s AND date IN %s",
[room_id, tuple(d.isoformat() for d in dates)]
)}
room = db.get_room(room_id)
return {d: overrides.get(d, room.base_price_cents) for d in dates}
Atomic Booking (Preventing Double-Booking)
class BookingService:
def create_booking(self, user_id: str, room_id: str, hotel_id: str,
check_in: date, check_out: date,
idempotency_key: str) -> Booking:
# Idempotency: return existing booking if key already used
existing = db.get_booking_by_idempotency_key(idempotency_key)
if existing:
return existing
with db.transaction():
# Lock room for the date range (prevents concurrent booking)
conflicting = db.query(
"""SELECT booking_id FROM bookings
WHERE room_id = %s
AND status IN ('CONFIRMED', 'CHECKED_IN', 'PENDING')
AND check_in %s
FOR UPDATE""", # pessimistic lock
[room_id, check_out, check_in]
)
if conflicting:
raise ValueError("Room is not available for the requested dates")
prices = AvailabilityService()._get_prices(
room_id, [(check_in + timedelta(days=i))
for i in range((check_out - check_in).days)]
)
total = sum(prices.values())
booking = Booking(
booking_id=generate_id(),
user_id=user_id,
room_id=room_id,
hotel_id=hotel_id,
check_in=check_in,
check_out=check_out,
status=BookingStatus.PENDING,
total_cents=total,
payment_intent_id='',
idempotency_key=idempotency_key,
created_at=datetime.utcnow(),
)
db.save(booking)
# Charge payment outside transaction (avoid holding DB lock during network call)
payment_result = payment_gateway.charge(
user_id=user_id,
amount_cents=total,
idempotency_key=f"pay_{idempotency_key}",
)
if payment_result.success:
booking.payment_intent_id = payment_result.intent_id
booking.status = BookingStatus.CONFIRMED
else:
booking.status = BookingStatus.CANCELLED
db.save(booking)
return booking
Cancellation and Refund Policy
CANCELLATION_POLICY = {
# (days_before_check_in) -> refund percentage
7: 100, # 7+ days before: full refund
3: 50, # 3-6 days before: 50% refund
0: 0, # Booking:
booking = db.get_booking(booking_id)
if booking.user_id != user_id:
raise PermissionError("Not your booking")
if booking.status != BookingStatus.CONFIRMED:
raise ValueError(f"Cannot cancel a {booking.status} booking")
days_before = (booking.check_in - date.today()).days
refund_pct = 0
for min_days, pct in sorted(CANCELLATION_POLICY.items(), reverse=True):
if days_before >= min_days:
refund_pct = pct
break
refund_cents = int(booking.total_cents * refund_pct / 100)
if refund_cents > 0:
payment_gateway.refund(booking.payment_intent_id, refund_cents)
booking.status = BookingStatus.CANCELLED
booking.cancelled_at = datetime.utcnow()
booking.refund_cents = refund_cents
db.save(booking)
return booking
Dynamic Pricing
def set_dynamic_price(room_id: str, date: date, price_cents: int):
"""Override room price for a specific date (holiday, event, low season)."""
db.upsert(RoomAvailability(
room_id=room_id, date=date,
is_available=True, price_cents=price_cents
))
def apply_seasonal_pricing(hotel_id: str):
"""Apply multipliers for peak seasons."""
PEAK_MULTIPLIER = 1.5
OFF_PEAK_MULTIPLIER = 0.8
rooms = db.get_rooms(hotel_id)
for room in rooms:
for month in range(1, 13):
multiplier = PEAK_MULTIPLIER if month in [6, 7, 8, 12] else OFF_PEAK_MULTIPLIER
# Set prices for all dates in this month
for day_offset in range(31):
try:
d = date(2025, month, day_offset + 1)
set_dynamic_price(room.room_id, d,
int(room.base_price_cents * multiplier))
except ValueError:
pass # invalid date (e.g., Feb 30)
Interview Questions
Q: How do you prevent two users from booking the same room for overlapping dates?
Use pessimistic locking: SELECT … FOR UPDATE on bookings rows that overlap the requested dates. Only one transaction can hold the lock — the concurrent request waits. After acquiring the lock, check for conflicts again (the double-check pattern). If no conflict, insert the new booking and commit. For higher throughput, use optimistic locking: attempt the INSERT with a unique constraint on (room_id, check_in, check_out) and handle the constraint violation as a conflict. PostgreSQL also supports exclusion constraints for range overlap: EXCLUDE USING gist (room_id WITH =, daterange(check_in, check_out) WITH &&).
Q: How would you scale search to handle millions of hotel listings?
Use Elasticsearch for search: index each hotel with geo_point, amenities, star_rating, and room types. For availability, the index includes a pre-computed “available dates” array updated asynchronously when bookings change. On search: Elasticsearch filters by city + date range (using the availability array) and ranks by relevance/price. The booking service remains the authoritative source — Elasticsearch may show slightly stale data, but the booking step does a real-time availability check against the database. This separation handles millions of queries/second without hitting the transactional database on every search.
Asked at: Airbnb Interview Guide
Asked at: Stripe Interview Guide
Asked at: Uber Interview Guide
Asked at: Shopify Interview Guide