Designing a hotel booking system
In this series (15 parts)
- Introduction to low level design
- SOLID principles
- Design patterns: Creational
- Design patterns: Structural
- Design patterns: Behavioral
- Designing a parking lot
- Designing a library management system
- Designing an elevator system
- Designing a hotel booking system
- Designing a ride-sharing model
- Designing a rate limiter
- Designing a logging framework
- Designing a notification system
- API design and contract-first development
- 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:
- A hotel has multiple room types (single, double, suite). Each type has a set of physical rooms.
- Guests can search for available rooms by date range and room type.
- Guests can book one or more rooms for a date range.
- The system prevents double-booking: a room cannot be booked by two guests for overlapping dates.
- Guests can cancel a booking. Cancellation fees depend on how close the cancellation is to check-in.
- 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
| Entity | Role |
|---|---|
| Hotel | Top-level aggregate, holds rooms |
| RoomType | Category with base price (single, double, suite) |
| Room | Physical room with a number and type |
| Booking | Links a guest to one or more rooms for a date range |
| Guest | Person making the booking |
| Payment | Financial 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
| Change | Impact |
|---|---|
| Add room amenities filter | New Amenity entity, join table to Room |
| Add group bookings | Booking already supports multiple rooms |
| Add loyalty discounts | New PricingRule implementation |
| Add waitlist for sold-out | New 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.