System Design Space
Knowledge graphSettings

Updated: April 10, 2026 at 6:40 PM

Hotel booking system

medium

Public interview at ArchDays 2022: availability calendars, overbooking, and correct reservation flow under contention.

Hotel booking does not usually break on the search page. It breaks on the availability calendar, where one room category is sold across date ranges and a small mistake quickly turns into oversell.

This chapter connects inventory by date, controlled overbooking, reservation confirmation, cancellations, and the path from a single service to sharded storage.

For interviews and engineering discussions, this case is useful because it forces you to talk about calendar correctness and contention around limited inventory instead of hiding behind generic marketplace language.

Availability Calendar

Inventory does not live in one number per product here. It lives in a date grid, so the model must handle stay ranges, cancellations, and limit recalculation cleanly.

Overbooking Window

Controlled oversell may be good for the business, but its limits need to be explicit in the model rather than hidden inside one service.

Room Contention

The most painful race happens on the last room of a category, so the confirmation path has to be designed for conflicts from the start.

Date Partitioning

Historical dates quickly inflate the tables, and dealing with older partitions becomes a separate operational concern.

This public ArchDays 2022 interview is a good example of why hotel booking is mostly a calendar-availability problem rather than a catalog problem. Once the business allows controlled overbooking, the architecture has to protect the room counters, the reservation-confirmation path, and the cancellation flow explicitly.

Related case

Designing Airbnb

A similar problem with availability calendars, dynamic pricing, and competition for limited inventory.

Читать обзор

Interview video

The full public interview is available on YouTube. It is useful to watch before reading the chapter so you can see how the requirements are clarified and how the design evolves step by step.

Watch on YouTube

Problem statement

Hotel booking system

Design a service for the Russian market that shows available hotel options, creates reservations for a chosen date range, and takes the user through payment and cancellation.

20,000 hotels

Rough order of magnitude for hotels in the system

1,000,000 rooms

Total number of rooms across all properties

Functional requirements

Hotel details

Detailed hotel information, room categories, and stay rules.

Create reservation

Pick dates, choose a room type, and confirm the booking online.

Payment

Integrate with the payment system and record the payment result.

Cancel reservation

Cancel an existing booking and return the room to available inventory.

Key business requirement: controlled oversell

The business intentionally allows a limited amount of overbooking because some guests cancel or never arrive. The architecture has to model this rule explicitly instead of treating it as an accidental side effect.

What is out of scope

Authentication and authorizationHotel admin panelFull-text hotel searchReviews and ratings

Load estimation

Start with a quick back-of-the-envelope estimate. It helps separate the places where scale really matters from the places where we should keep the design simple.

Average stay length5 nights
Average occupancy70%
Occupied rooms per day700,000
New bookings per day~200,000
Average load~2.3 TPS

Important: the average is small, but seasonal peaks, holidays, and promotions can easily create 10x–100x spikes. The system should be designed for those short windows of heavy contention as well.

Core API

GET/v1/hotels/{hotel_id}

Fetch the hotel page and general offer information

GET/v1/hotels/{hotel_id}/rooms/{room_type_id}

Fetch details for a specific room category

POST/v1/reservations

Create a reservation for a chosen date range

{
  "hotel_id": "123",
  "room_type_id": "456",
  "start_date": "2024-03-15",
  "end_date": "2024-03-20",
  "guest_info": { ... }
}
DELETE/v1/reservations/{reservation_id}

Cancel a reservation

What the API should define early

In a real system, reservation creation and payment callbacks need clear idempotency rules so a retry after a timeout does not create a second booking or charge the guest twice.

Data-model evolution

The real question is not how to store a reservation row. It is how to model availability per room type and per date so that reads stay cheap and updates stay safe.

Naive approach: one reservations table

Reservation
├── id
├── hotel_id
├── room_type_id
├── room_id (specific room)
├── start_date
├── end_date
├── status
└── guest_id

Problem 1: availability checks require COUNT across reservations for every requested date.

Problem 2: controlled oversell becomes a separate layer of custom business logic.

Problem 3: concurrent requests can race for the last room of a category.

Better approach: inventory by date

RoomTypeInventory
├── hotel_id
├── room_type_id
├── date
├── total_rooms (total rooms of this type)
├── total_reserved (reserved)
└── overbooking_limit (overbooking limit)

Constraint: total_reserved <= total_rooms * (1 + overbooking_limit)

Advantage 1: availability reads become a point lookup instead of a scan across all reservations.

Advantage 2: controlled oversell is now an explicit part of the model.

Advantage 3: the counter can be updated atomically with a conditional UPDATE.

Handling concurrency

The critical reservation path needs strict correctness: if two users try to book the last room at the same time, the system must still confirm only the allowed number of reservations.

In practice, the design usually comes down to optimistic versus pessimistic locking. The real comparison is not just correctness, but also throughput under peak demand and the risk of deadlocks.

Pessimistic locking

BEGIN;
SELECT * FROM room_inventory 
WHERE hotel_id = ? AND room_type_id = ? AND date = ?
FOR UPDATE;

-- Check the limit and update the counter
UPDATE room_inventory SET total_reserved = total_reserved + 1 ...

COMMIT;

Pros

Straightforward logic and very explicit correctness guarantees

Cons

Row locking, lower throughput, and deadlock risk

Optimistic locking

-- Isolation Level: REPEATABLE READ
BEGIN;

UPDATE room_inventory 
SET total_reserved = total_reserved + 1
WHERE hotel_id = ? 
  AND room_type_id = ? 
  AND date = ?
  AND total_reserved < total_rooms * (1 + overbooking_limit);

-- If affected_rows = 0, the limit is already exhausted
COMMIT;

Pros

Higher throughput, no explicit row locks, atomic conditional update

Cons

Requires retries and careful conflict handling

Database-level constraint

ALTER TABLE room_inventory 
ADD CONSTRAINT check_overbooking 
CHECK (total_reserved <= total_rooms * (1 + overbooking_limit));

This is the final guardrail: even if the service code is wrong, the database still prevents the system from crossing the allowed limit.

A reasonable choice for this scale

For an average load around 2.3 TPS and peaks up to 100 TPS, optimistic locking is usually the more practical choice. It keeps the design simple while leaving enough headroom for spikes. The pessimistic option becomes more interesting only when contention on a specific inventory slice turns extreme.

Scaling strategies

The system usually grows along two axes: time and the number of hotels. At that point, you need a clear story for partitioning by date, sharding by hotel, using consistent hashing when needed, and keeping maintenance work manageable.

Time partitioning

Inventory tables map naturally to months or quarters because booking is date-driven by design.

  • Historical periods are easier to archive and prune
  • Queries touch a narrower slice of data
  • Operational work on old partitions becomes predictable

Partitioning by hotel_id

Once one cluster is no longer enough, data can be distributed across shards by hotel identifier.

  • Write load and inventory counters scale out more cleanly
  • Heavy hotel chains stop interfering with the rest of the system
  • Shard assignment can be based on consistent hashing

Key takeaways

1

Start with the invariants

If you do not clarify the date-range model and the oversell policy early, you can build a neat but fundamentally wrong architecture.

2

Show the model evolving

A strong answer rarely starts with the perfect schema. It is better to show why the naive reservations table fails and how you move toward inventory by date.

3

Explain the concurrency choice

Optimistic and pessimistic locking differ by trade-off, not by abstract correctness alone. The interviewer wants to hear why one fits this workload better than the other.

4

Let scaling follow the shape of the data

Partitioning by time, inventory counters, and hotel-based sharding should all follow from the calendar nature of the problem rather than appear as a generic list of “things to mention”.

Related chapters

Enable tracking in Settings