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.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent double-booking of a hotel room?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use pessimistic locking: SELECT … FOR UPDATE on bookings rows with overlapping dates. The SQL overlap condition: check_in requested_check_in. Only one transaction holds the lock per room u2014 concurrent requests wait. After acquiring the lock, verify no overlap exists (double-check), then insert the new booking. For PostgreSQL specifically, exclusion constraints handle this elegantly: EXCLUDE USING gist (room_id WITH =, daterange(check_in, check_out) WITH &&). This enforces no two bookings can have the same room_id with overlapping date ranges, using a GiST index for efficient enforcement.”
}
},
{
“@type”: “Question”,
“name”: “How do you handle the two-phase booking flow (hold then confirm)?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Create a PENDING booking when the user selects dates. Set a hold TTL (e.g., 10 minutes). During this window, the room is effectively reserved u2014 conflict detection treats PENDING as occupied. After payment confirmation, transition to CONFIRMED. If the user abandons (TTL expires), a background job cancels the PENDING booking, releasing the room. This prevents users from selecting a room, going to the payment page, and finding it gone when they return. The TTL must be short enough to not block other users long but long enough for a normal checkout flow.”
}
},
{
“@type”: “Question”,
“name”: “How do you implement dynamic pricing for a hotel?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store a room_availability table with (room_id, date, price_cents). When pricing override exists for a date, use it; otherwise fall back to the room’s base_price_cents. Populate overrides via: (1) Manual override by hotel managers. (2) Seasonal pricing jobs that apply multipliers for peak months. (3) Demand-based pricing u2014 if occupancy for a date exceeds 80%, increase prices 20%. (4) Last-minute deals u2014 if a room is unsold 3 days before the date, discount by 15%. The booking system reads prices from this table when calculating total; historical prices are preserved on the booking record.”
}
},
{
“@type”: “Question”,
“name”: “How does the cancellation refund policy work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Store the cancellation policy on the booking at time of creation (not the current policy u2014 policies can change). Common tiers: 7+ days before check-in = 100% refund, 3-6 days = 50%, < 3 days = 0%. On cancellation: compute days_before_checkin = (check_in – today). Look up the refund percentage from the policy. Issue a partial or full refund via the payment gateway (Stripe refund API). Update booking status to CANCELLED and record refund_cents. For non-refundable rates, store refund_percent=0 at booking time. Always store the actual refund issued on the booking record for customer service."
}
},
{
"@type": "Question",
"name": "How would you build the search feature for millions of hotel listings?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use Elasticsearch for search: index hotels with geo_point (for proximity search), city, amenities, star_rating, and room_types. For availability, asynchronously update an "available_for" array per hotel listing whenever a booking is created or cancelled. On search: filter by geo_distance from the requested city, filter room_type and amenities, and filter by has_availability for the date range. Rank by price + rating + distance. This lets Elasticsearch serve millions of search queries without hitting the transactional database. Availability data may lag by seconds u2014 the booking step does a real DB check, so stale search results at worst cause a "no longer available" error."
}
}
]
}
Asked at: Airbnb Interview Guide
Asked at: Stripe Interview Guide
Asked at: Uber Interview Guide
Asked at: Shopify Interview Guide