Search…
Low Level Design · Part 9

Designing a hotel booking system

In this series (15 parts)
  1. Introduction to low level design
  2. SOLID principles
  3. Design patterns: Creational
  4. Design patterns: Structural
  5. Design patterns: Behavioral
  6. Designing a parking lot
  7. Designing a library management system
  8. Designing an elevator system
  9. Designing a hotel booking system
  10. Designing a ride-sharing model
  11. Designing a rate limiter
  12. Designing a logging framework
  13. Designing a notification system
  14. API design and contract-first development
  15. Data modeling for system design

Hotel booking is a transactional design problem. The core challenge is not complex algorithms but data integrity: two guests must never end up with the same room on the same night. This article walks through the entity model, the availability check, the booking flow, and the cancellation policy, each building on patterns from previous articles in this series.

Requirements

Functional:

  1. A hotel has multiple room types (single, double, suite). Each type has a set of physical rooms.
  2. Guests can search for available rooms by date range and room type.
  3. Guests can book one or more rooms for a date range.
  4. The system prevents double-booking: a room cannot be booked by two guests for overlapping dates.
  5. Guests can cancel a booking. Cancellation fees depend on how close the cancellation is to check-in.
  6. Payment is collected at booking time. Refunds follow the cancellation policy.

Non-functional:

  • The availability check and booking must be atomic to prevent race conditions.
  • The system should support multiple hotels (multi-tenant).

Entities

EntityRole
HotelTop-level aggregate, holds rooms
RoomTypeCategory with base price (single, double, suite)
RoomPhysical room with a number and type
BookingLinks a guest to one or more rooms for a date range
GuestPerson making the booking
PaymentFinancial transaction tied to a booking

Class diagram

classDiagram
  class Hotel {
      -String id
      -String name
      -String address
      -List~Room~ rooms
      +searchAvailable(DateRange range, RoomType type) List~Room~
  }

  class RoomType {
      <<enumeration>>
      SINGLE
      DOUBLE
      SUITE
  }

  class Room {
      -String roomId
      -int roomNumber
      -int floor
      -RoomType type
      -decimal basePrice
  }

  class Booking {
      -String bookingId
      -Guest guest
      -List~Room~ rooms
      -LocalDate checkIn
      -LocalDate checkOut
      -BookingStatus status
      -LocalDateTime createdAt
      +totalNights() int
      +totalPrice() decimal
      +cancel() void
  }

  class Guest {
      -String guestId
      -String name
      -String email
      -String phone
  }

  class Payment {
      -String paymentId
      -Booking booking
      -decimal amount
      -PaymentMethod method
      -PaymentStatus status
      -LocalDateTime paidAt
      +process() boolean
      +refund(decimal amount) boolean
  }

  class BookingStatus {
      <<enumeration>>
      CONFIRMED
      CHECKED_IN
      CHECKED_OUT
      CANCELLED
  }

  class PaymentStatus {
      <<enumeration>>
      PENDING
      COMPLETED
      REFUNDED
      FAILED
  }

  Hotel "1" *-- "many" Room
  Room --> RoomType
  Booking "1" --> "1..*" Room
  Booking "1" --> "1" Guest
  Payment "1" --> "1" Booking
  Booking --> BookingStatus
  Payment --> PaymentStatus

Class diagram for the hotel booking system.

Data model

The ER diagram reveals relationships that the class diagram alone does not capture, especially the many-to-many between bookings and rooms (a booking can have multiple rooms, and a room appears in multiple bookings across time).

erDiagram
  HOTEL ||--|{ ROOM : contains
  ROOM }|--|| ROOM_TYPE : categorizedAs
  BOOKING ||--|{ BOOKING_ROOM : includes
  BOOKING_ROOM }|--|| ROOM : references
  BOOKING ||--|| GUEST : madeBy
  PAYMENT ||--|| BOOKING : settles

  HOTEL {
      string id PK
      string name
      string address
  }
  ROOM {
      string room_id PK
      string hotel_id FK
      int room_number
      int floor
      string room_type
      decimal base_price
  }
  ROOM_TYPE {
      string name PK
      string description
  }
  GUEST {
      string guest_id PK
      string name
      string email
      string phone
  }
  BOOKING {
      string booking_id PK
      string guest_id FK
      date check_in
      date check_out
      string status
      datetime created_at
  }
  BOOKING_ROOM {
      string booking_id FK
      string room_id FK
  }
  PAYMENT {
      string payment_id PK
      string booking_id FK
      decimal amount
      string method
      string status
      datetime paid_at
  }

Entity-relationship diagram for the hotel booking database.

Availability check

The availability query is the heart of this system. A room is available for a date range if no confirmed booking overlaps with that range.

SELECT r.room_id, r.room_number
FROM room r
WHERE r.hotel_id = :hotelId
  AND r.room_type = :roomType
  AND r.room_id NOT IN (
      SELECT br.room_id
      FROM booking_room br
      JOIN booking b ON br.booking_id = b.booking_id
      WHERE b.status IN ('CONFIRMED', 'CHECKED_IN')
        AND b.check_in < :checkOut
        AND b.check_out > :checkIn
  )

The overlap condition check_in < :checkOut AND check_out > :checkIn is the standard interval overlap test. If you get this wrong, you will either allow double-bookings or incorrectly block available rooms.

In Java, the equivalent method:

public List<Room> searchAvailable(String hotelId, RoomType type,
                                   LocalDate checkIn, LocalDate checkOut) {
    return rooms.stream()
        .filter(r -> r.getType() == type)
        .filter(r -> !hasOverlappingBooking(r, checkIn, checkOut))
        .collect(Collectors.toList());
}

private boolean hasOverlappingBooking(Room room, LocalDate checkIn, LocalDate checkOut) {
    return bookings.stream()
        .filter(b -> b.getStatus() != BookingStatus.CANCELLED)
        .filter(b -> b.getRooms().contains(room))
        .anyMatch(b -> b.getCheckIn().isBefore(checkOut)
                    && b.getCheckOut().isAfter(checkIn));
}

Booking flow

sequenceDiagram
  participant G as Guest
  participant BS as BookingService
  participant DB as Database
  participant PS as PaymentService

  G->>BS: searchAvailable(dates, type)
  BS->>DB: query available rooms
  DB-->>BS: available rooms list
  BS-->>G: show available rooms

  G->>BS: createBooking(rooms, dates)
  BS->>DB: BEGIN TRANSACTION
  BS->>DB: Lock selected rooms for date range
  BS->>DB: Verify rooms still available
  alt rooms available
      BS->>DB: INSERT booking
      BS->>PS: processPayment(amount)
      PS-->>BS: payment confirmed
      BS->>DB: INSERT payment record
      BS->>DB: COMMIT
      BS-->>G: booking confirmed
  else rooms taken
      BS->>DB: ROLLBACK
      BS-->>G: rooms no longer available
  end

Sequence diagram for the booking flow with payment.

The critical detail: the availability check and the booking insert happen inside the same database transaction. Without this, two guests searching at the same time could both see the room as available, both attempt to book, and one would succeed while the other gets a cryptic error, or worse, both succeed.

Preventing double-booking

Three approaches, in increasing order of robustness:

1. Application-level check: Query availability, then insert. Race condition exists between the two operations. Do not rely on this alone.

2. Database transaction with row locking: Use SELECT ... FOR UPDATE on the room rows within a transaction. This serializes concurrent booking attempts for the same room.

BEGIN;
SELECT room_id FROM room WHERE room_id IN (:roomIds) FOR UPDATE;
-- verify no overlapping bookings exist
INSERT INTO booking (...) VALUES (...);
INSERT INTO booking_room (...) VALUES (...);
COMMIT;

3. Unique constraint approach: Create a table room_night with one row per room per night. A unique constraint on (room_id, date) makes double-booking physically impossible at the database level.

CREATE TABLE room_night (
    room_id VARCHAR(36),
    night DATE,
    booking_id VARCHAR(36),
    PRIMARY KEY (room_id, night)
);

When creating a booking for room 101 from Jan 5 to Jan 7, insert rows for Jan 5 and Jan 6. If any insert violates the primary key, the booking fails. This is the most robust approach.

Cancellation policy

Cancellation fees depend on timing. Model this with the Strategy pattern:

public interface CancellationPolicy {
    decimal calculateRefund(Booking booking, LocalDate cancellationDate);
}

public class StandardCancellationPolicy implements CancellationPolicy {
    public decimal calculateRefund(Booking booking, LocalDate cancellationDate) {
        long daysBeforeCheckIn = ChronoUnit.DAYS.between(cancellationDate, booking.getCheckIn());

        if (daysBeforeCheckIn >= 7) {
            return booking.totalPrice(); // full refund
        } else if (daysBeforeCheckIn >= 3) {
            return booking.totalPrice().multiply(new BigDecimal("0.50")); // 50% refund
        } else {
            return BigDecimal.ZERO; // no refund
        }
    }
}

Different hotels or room types can have different policies. The Booking does not need to know which policy applies; the BookingService selects the right one.

Price calculation

Room pricing is rarely just basePrice * nights. Real systems layer on seasonal rates, weekend surcharges, and promotional discounts. Keep these composable:

public interface PricingRule {
    decimal apply(Room room, LocalDate date, decimal currentPrice);
}

public class WeekendSurcharge implements PricingRule {
    public decimal apply(Room room, LocalDate date, decimal currentPrice) {
        if (date.getDayOfWeek() == DayOfWeek.FRIDAY
            || date.getDayOfWeek() == DayOfWeek.SATURDAY) {
            return currentPrice.multiply(new BigDecimal("1.20"));
        }
        return currentPrice;
    }
}

Apply rules in a chain. The total booking price is the sum of per-night prices after all rules are applied.

Extensibility

ChangeImpact
Add room amenities filterNew Amenity entity, join table to Room
Add group bookingsBooking already supports multiple rooms
Add loyalty discountsNew PricingRule implementation
Add waitlist for sold-outNew Waitlist class, observer on cancellation

What comes next

The ride-sharing model brings location-aware matching and a more complex state machine for trip lifecycles. It also introduces the pricing strategy pattern in a dynamic context where fares change based on demand.

Start typing to search across all content
navigate Enter open Esc close