Skip to main content
Create your own
Lesson illustration

Hotel Reservation System Design

Hello! Welcome to our next case study on booking systems.

In our last lesson, we designed a movie ticket booking system. We saw that the core challenge was handling concurrency to prevent double-booking of seats. Our solution involved an atomic "check-and-lock" operation on a Show-specific object, abstracting the locking mechanism using the Strategy pattern.

Today, we'll tackle a related but more complex problem: designing a hotel reservation system. Your learning outcome is to design a system that manages room availability, bookings, and cancellations.

While this sounds similar to booking movie tickets, the key difference lies in the nature of the resource. Instead of booking a discrete seat for a fixed time, users book a room of a certain type over a date range. This introduces new challenges for managing inventory and ensuring transactional integrity.

We will follow our structured design process to build a solution, focusing on the data models and concurrency strategies appropriate for this new challenge.

Step 1: Clarifying Requirements

As always, we begin by turning a vague prompt into a concrete set of requirements. For a hotel reservation system, the core functionalities are fairly standard.

Let's start by reviewing a common set of requirements for such a system to establish our scope.

Designing a Scalable Hotel Booking System: An In-Depth ...

The article 'Designing a Scalable Hotel Booking System' provides a clear list of functional and non-functional requirements. This will help us define the boundaries of our design.

Please read Section 2, 'Functional Requirements', and Section 3, 'Non-Functional Requirements'. Focus on understanding the core user actions (search, book, cancel) and the critical system quality of 'Data Consistency'.

Based on this, let's summarize our core requirements:

  • Functional Requirements:

    1. Search: Guests can search for available rooms based on hotel, date range, and room type (e.g., Standard, Deluxe, Suite).
    2. Booking: Guests can book an available room for a specified date range.
    3. Cancellation: Guests can cancel an existing booking. The system should handle any refund logic (e.g., full refund if canceled 24 hours prior).
    4. Management: Admins/Receptionists can add/edit rooms, room types, and manage bookings.
    5. Notifications: The system should send notifications for events like booking confirmation or cancellation.
  • Key Non-Functional Requirement:

    • High Consistency: The system must never show a room as available when it is not, and it must never allow a room to be double-booked. This means our booking and inventory update operations must be atomic and durable (the 'A' and 'D' in ACID).

Step 2: Identifying Core Entities and Their Relationships

With the requirements clear, we can identify the main classes. In an interview, you'd sketch this out on a whiteboard.

The primary entities include:

  • Hotel: Represents a hotel property, containing rooms.
  • Room: Represents a specific physical room with a number, style, and status.
  • User (and its subtypes like Guest, Receptionist): Represents the actors interacting with the system.
  • RoomBooking: The central entity that captures a reservation, linking a User, a Room (or room type), dates, and status.
  • Invoice/Payment: Entities to handle the financial aspects of a booking.

Let's watch a video that walks through creating these classes and their relationships in Java, which aligns perfectly with your background.

Google Interview Question Solved| Low Level Design Complete Code | Hotel Management System | Part 2

The video 'Google Interview Question Solved| Low Level Design Complete Code' by Soumyajit Bhattacharyay provides an excellent, detailed walkthrough of the class structure for a hotel management system.

Please watch from 01:05 to 15:47. As you watch, pay attention to the following design choices: Core Classes: How Hotel, Location, and Room are defined with their attributes. Enums: The use of enums for RoomStyle and RoomStatus to represent fixed sets of values. Inheritance: How a base Person class is used for different actors like Guest and Receptionist. Composition: How Search and Booking logic is encapsulated in separate classes and composed into Guest and Receptionist to avoid code duplication. Decorator Pattern: The clever use of the Decorator pattern to calculate totalRoomCharge by adding services to a base charge.

This detailed class structure provides a solid foundation. Here is a UML diagram that visualizes these relationships at a high level.

UML Class Diagram for Hotel Reservation System
This UML class diagram shows the main entities (`Hotel`, `Room`, `Booking`, `User`) and their relationships, providing a blueprint for our system's object model.

Step 3: The Core Challenge - Inventory Management and Concurrency

Here lies the most difficult part of the problem. How do we efficiently and safely manage room availability for a given date range?

Let's say a hotel has 10 "Deluxe" rooms.

  • User A wants to book one Deluxe room from Day 3 to Day 7.
  • User B wants to book one Deluxe room from Day 5 to Day 9.

The system needs to check if at least one Deluxe room is available on Day 3, 4, 5, 6, AND 7 for User A. If User A books it, the available count for those days drops to 9. When User B then searches, the system must see that on Days 5, 6, and 7, only 9 rooms are left.

A race condition occurs if User A and User B try to book the last available Deluxe room for an overlapping period (e.g., Day 5) at the same time.

Designing the Inventory Schema

The foundation of our solution is the database schema for inventory. A highly effective approach is to have a dedicated table that tracks the number of available rooms for each room type, for each day.

Let's examine a professional schema design for this.

Designing a Scalable Hotel Booking System: An In-Depth ...

The 'Designing a Scalable Hotel Booking System' article proposes a clean and effective schema for an Inventory Service.

In Section 4, 'Core Entities & Data Models', find the 'Inventory Service Database (PostgreSQL)' subsection. Focus on the structure of the DailyAvailability table.

The proposed DailyAvailability table is structured as follows:

Column Type Description
daily_availability_id PK Unique ID for the row.
room_type_id FK Links to the RoomType table.
date Date The specific date for this inventory count.
available_rooms Integer Number of rooms of this type available on this date.
version Integer A counter for optimistic locking.

When a user wants to book a room from startDate to endDate, our system must:

  1. Query the DailyAvailability table for every date in that range.
  2. Check if available_rooms > 0 for all those dates.
  3. If yes, decrement available_rooms for all those dates.

This multi-row update must be atomic. If the system decrements the count for 3 out of 5 days and then crashes, the inventory data becomes corrupt.

Step 4: The Booking Workflow and Transaction Management

To ensure atomicity, we must wrap the entire booking operation—checking availability, creating the booking record, and updating inventory—within a database transaction.

In a modern framework like Spring Boot, this is remarkably straightforward using the @Transactional annotation.

Here is a simplified BookingService in Java that illustrates the flow:

@Service
public class BookingService {

    @Autowired
    private DailyAvailabilityRepository availabilityRepo;
    
    @Autowired
    private BookingRepository bookingRepo;

    @Transactional
    public Booking createBooking(String userId, String roomTypeId, LocalDate startDate, LocalDate endDate) {
        
        // 1. Fetch availability for the entire date range.
        List<DailyAvailability> availabilities = availabilityRepo.findAllByRoomTypeIdAndDateBetween(roomTypeId, startDate, endDate);

        // This requires a database lock to prevent race conditions. More on this below.

        // 2. Validate that rooms are available for the entire duration.
        if (availabilities.size() != (ChronoUnit.DAYS.between(startDate, endDate) + 1)) {
            throw new NotAvailableException("Dates not available for booking.");
        }
        for (DailyAvailability day : availabilities) {
            if (day.getAvailableRooms() <= 0) {
                throw new NotAvailableException("Room type not available for the selected dates.");
            }
        }

        // 3. Decrement the count for each day.
        for (DailyAvailability day : availabilities) {
            day.setAvailableRooms(day.getAvailableRooms() - 1);
            availabilityRepo.save(day); // The ORM will generate an UPDATE statement.
        }

        // 4. Create the booking record.
        Booking newBooking = new Booking(userId, roomTypeId, startDate, endDate);
        newBooking.setStatus(BookingStatus.CONFIRMED);
        
        return bookingRepo.save(newBooking);
    } // The transaction commits here. If any exception was thrown, it would roll back.
}

Concurrency Control Strategies

The @Transactional annotation ensures atomicity, but it doesn't by itself prevent the race condition we discussed. We still need a locking mechanism.

  1. Pessimistic Locking: This is the most direct approach. We ask the database to lock the rows we are reading so no other transaction can modify them until ours is finished.

    • How: In the database query that fetches the DailyAvailability rows, you would use a SELECT ... FOR UPDATE clause. Most Java ORMs, like JPA/Hibernate, support this. The database itself handles the lock, blocking other transactions that try to write to the same rows.
    • Pros: Very safe. Guarantees consistency.
    • Cons: Can reduce concurrency if transactions are long.
  2. Optimistic Locking: This strategy assumes conflicts are rare. Instead of locking, it checks if the data has changed before committing.

    • How: This is what the version column in our DailyAvailability table is for.
      1. Read the row, including its version number (e.g., version = 5).
      2. When you issue the UPDATE statement, you add a condition: UPDATE DailyAvailability SET available_rooms = ... WHERE id = ... AND version = 5.
      3. The database returns the number of rows affected. If it's 0, it means another transaction updated the row and incremented its version in the meantime.
      4. Your application logic would then catch this, roll back, and can retry the whole transaction.
    • Pros: More scalable than pessimistic locking as it doesn't hold database locks, leading to higher throughput.
    • Cons: More complex to implement application-level retry logic.

For an LLD interview, explaining both approaches and their trade-offs demonstrates a deep understanding. The pessimistic SELECT ... FOR UPDATE is often simpler to reason about and implement.

Test your understanding!

Imagine you are implementing the cancelBooking method. What steps must this method perform, and why is it also critical to wrap it in a database transaction?

Show answer

The cancelBooking method must perform the reverse operations of createBooking:

  1. Find the Booking record to be canceled.
  2. Update the Booking status to CANCELLED.
  3. For every day in the booking's date range, find the corresponding DailyAvailability row and increment the available_rooms count.

It is critical to wrap this in a transaction to ensure atomicity. If the system updated the booking status to CANCELLED but crashed before it could increment the room counts, the rooms would be lost to the system—they would never become available for re-booking. A transaction ensures that both the booking status update and all inventory updates succeed together or fail (and roll back) together.

Step 5: High-Level Design Considerations (Preview)

Our design so far works perfectly for a single, monolithic application. But what happens in a large-scale microservices architecture where the BookingService and InventoryService are separate applications talking to different databases?

A diagram showing a Booking Service and an Inventory Service. The challenge is ensuring an update to both is atomic.

A simple database transaction (@Transactional) can't span two different databases. This is a classic distributed transaction problem.

Let's watch a short clip that explains why this is hard and introduces a common solution. This is an HLD topic, but it's important to know the limitations of our LLD solution.

System Design: Hotel Booking

The video 'System Design: Hotel Booking' from System Design Fight Club explains the partial failure problem in distributed systems and introduces the idea of a coordinator to solve it.

Please watch from 12:06 to 17:58. Don't worry about the specific technologies like Zookeeper or two-phase commit. Focus on understanding the problem: what happens if the system fails between updating inventory and creating the reservation? The key takeaway is the need for a 'coordinator' that ensures the transaction is either fully completed or fully rolled back across all services.

In a real-world system, this is often solved with patterns like Saga or Two-Phase Commit, which you will encounter in HLD interviews. For our LLD context, knowing that a single-database transaction is the solution for a monolithic app, and that more complex patterns are needed for microservices, is a sign of a well-rounded engineer.

Conclusion

Great job! We have designed a hotel reservation system, tackling its unique challenges.

Key Takeaways:

  • Identify the Resource Type: The design changes significantly based on whether the resource is discrete (a specific seat) or a quantity over a range (rooms of a certain type).
  • Schema is Key: A well-designed inventory schema (like DailyAvailability) is the foundation for managing availability and concurrency.
  • Use Database Power: Leverage database features like transactions (@Transactional) for atomicity and locking (Pessimistic or Optimistic) for concurrency control. This is often simpler and safer than application-level locks for this use case.
  • Know Your Boundaries: Understand that simple database transactions work for a single database. When you move to a distributed microservices architecture, you enter the world of distributed transactions, which require more advanced patterns.

In our next lesson, we'll switch gears completely and design a chess game. This will move us away from booking and concurrency and into the domain of state management, rules engines, and modeling complex interactions.

Can't find a good explanation? Sign up and we'll make it for you

Sign up