← Back to Design & Development
Low-Level Design

Hotel Management System

End-to-end design — Booking, Search, Cancellation, Housekeeping, Payments with full MySQL schema & ER diagrams

Step 1

Clarify Requirements

A Hotel Management System lets managers handle online hotel operations — rooms, bookings, billing, housekeeping — from a single portal. Guests can search & book rooms online.

✅ Functional Requirements

  • Search rooms by date, location, room-type
  • Book different room types: Standard, Deluxe, Family Suite
  • Retrieve "who booked room X" / "all rooms booked by user Y"
  • Cancel booking — full refund if > 24hrs before check-in
  • Send notifications near check-in / check-out
  • Housekeeping log per room
  • Add room-service, food, amenities to booking
  • Payment via credit card, check, or cash

❌ Out of Scope (clarify first)

  • Multi-hotel chains / franchise distribution
  • Loyalty / rewards program
  • Dynamic pricing / surge engine
  • Review & rating system
  • Third-party OTA integration (MMT, Booking)
  • Staff payroll / HR module
Scale assumptions: ~100 hotels · 500 rooms/hotel · 10K concurrent searches · 1K bookings/day. Reads >> writes → cache aggressively on search, transactional consistency on booking.
Step 2

Actors & Use Cases

👤 Guest

  • Search rooms
  • Book / cancel
  • Add services
  • Pay bill

🧑‍💼 Receptionist

  • Check-in / out
  • Walk-in booking
  • Issue invoice

🧹 Housekeeper

  • View task log
  • Mark room clean
  • Report damage

🧑‍🔧 Manager / Admin

  • Add rooms
  • View reports
  • Manage staff
flowchart LR G([Guest]) R([Receptionist]) H([Housekeeper]) M([Manager]) G --> S[Search Rooms] G --> B[Book Room] G --> C[Cancel Booking] G --> P[Pay Bill] G --> A[Add Service/Amenity] R --> CI[Check-In] R --> CO[Check-Out] R --> WI[Walk-in Booking] H --> HT[View Housekeeping Log] H --> MC[Mark Room Clean] M --> AR[Add/Edit Rooms] M --> RP[View Reports] style G fill:#e8743b,stroke:#e8743b,color:#fff style R fill:#4a90d9,stroke:#4a90d9,color:#fff style H fill:#38b265,stroke:#38b265,color:#fff style M fill:#9b72cf,stroke:#9b72cf,color:#fff
Step 3

Core Entities & Enums

Before tables, identify the domain objects. Each maps to a class in code and (usually) a table in DB.

🏨 Hotel

Has a location and many rooms. Identifiable by id + name.

🛏️ Room

Belongs to a hotel. Has a type, a style, a status, and a price.

📅 Booking

Guest × Room × Date-range. Tracks total, status, payment.

👤 User

Base for Guest / Receptionist / Housekeeper / Manager (role).

💳 Payment

One-to-one with booking. Supports Card / Cash / Cheque.

🧹 Housekeeping Log

Task per room — cleaning, laundry, damage report.

🍽️ Service Charge

Food, laundry, minibar — line items on a booking.

🔔 Notification

Reminder near check-in / check-out. Email / SMS / Push.

🏷️ RoomType

Enum — STANDARD, DELUXE, FAMILY_SUITE, PRESIDENTIAL.

enum · Java
public enum RoomType {
  STANDARD, DELUXE,
  FAMILY_SUITE, PRESIDENTIAL
}

public enum RoomStatus {
  AVAILABLE, BOOKED,
  OCCUPIED, UNDER_MAINTENANCE
}
enum · Java
public enum BookingStatus {
  CONFIRMED, CHECKED_IN,
  CHECKED_OUT, CANCELLED
}

public enum PaymentMode {
  CREDIT_CARD, CASH, CHEQUE
}
Step 4

Class Diagram

High-level OOP view — the classes, their fields, and relationships. Drives both the Java model layer and DB tables.

classDiagram class Hotel { +Long id +String name +Address address +List~Room~ rooms +addRoom(Room) } class Room { +Long id +String roomNumber +RoomType type +RoomStatus status +BigDecimal pricePerNight +isAvailable(from, to) bool } class User { +Long id +String name +String email +String phone +Role role } class Guest class Receptionist class Housekeeper class Manager class Booking { +Long id +Long guestId +Long roomId +LocalDate checkIn +LocalDate checkOut +BookingStatus status +BigDecimal total +confirm() +cancel() } class Payment { +Long id +Long bookingId +BigDecimal amount +PaymentMode mode +PaymentStatus status } class ServiceCharge { +Long id +Long bookingId +String description +BigDecimal amount } class HousekeepingLog { +Long id +Long roomId +Long staffId +TaskType taskType +LocalDateTime performedAt } class Notification { +Long id +Long userId +String message +Channel channel } User <|-- Guest User <|-- Receptionist User <|-- Housekeeper User <|-- Manager Hotel "1" *-- "many" Room Guest "1" --> "many" Booking Booking "many" --> "1" Room Booking "1" --> "1" Payment Booking "1" --> "many" ServiceCharge Room "1" --> "many" HousekeepingLog User "1" --> "many" Notification
Step 5

ER Diagram — MySQL Schema

Maps the class diagram into relational tables. PK / FK constraints and indexes shown.

erDiagram HOTEL ||--o{ ROOM : has HOTEL { BIGINT id PK VARCHAR name VARCHAR city VARCHAR country } ROOM { BIGINT id PK BIGINT hotel_id FK VARCHAR room_number ENUM type ENUM status DECIMAL price_per_night } USER ||--o{ BOOKING : makes USER { BIGINT id PK VARCHAR name VARCHAR email VARCHAR phone ENUM role } BOOKING ||--|| PAYMENT : has BOOKING ||--o{ SERVICE_CHARGE : includes BOOKING ||--o{ NOTIFICATION : triggers ROOM ||--o{ BOOKING : booked_in ROOM ||--o{ HOUSEKEEPING_LOG : logged BOOKING { BIGINT id PK BIGINT guest_id FK BIGINT room_id FK DATE check_in DATE check_out ENUM status DECIMAL total_amount DATETIME created_at } PAYMENT { BIGINT id PK BIGINT booking_id FK DECIMAL amount ENUM mode ENUM status DATETIME paid_at } SERVICE_CHARGE { BIGINT id PK BIGINT booking_id FK VARCHAR description DECIMAL amount DATETIME added_at } HOUSEKEEPING_LOG { BIGINT id PK BIGINT room_id FK BIGINT staff_id FK ENUM task_type TEXT notes DATETIME performed_at } NOTIFICATION { BIGINT id PK BIGINT user_id FK BIGINT booking_id FK ENUM channel TEXT message DATETIME sent_at }
Step 6

MySQL Schema — Full DDL

Complete production-ready DDL with PKs, FKs, composite indexes, and constraints to catch overlapping bookings.

sql · hotel.sql
-- ════════════════ 1. HOTEL ════════════════
CREATE TABLE hotel (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  name         VARCHAR(200) NOT NULL,
  address_line VARCHAR(500),
  city         VARCHAR(100) NOT NULL,
  state        VARCHAR(100),
  country      VARCHAR(100) NOT NULL,
  created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_hotel_location (city, country)
);

-- ════════════════ 2. USER ════════════════
CREATE TABLE user (
  id         BIGINT AUTO_INCREMENT PRIMARY KEY,
  name       VARCHAR(150) NOT NULL,
  email      VARCHAR(200) NOT NULL UNIQUE,
  phone      VARCHAR(20),
  password_hash VARCHAR(255),
  role       ENUM('GUEST','RECEPTIONIST','HOUSEKEEPER','MANAGER') NOT NULL DEFAULT 'GUEST',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- ════════════════ 3. ROOM ════════════════
CREATE TABLE room (
  id              BIGINT AUTO_INCREMENT PRIMARY KEY,
  hotel_id        BIGINT NOT NULL,
  room_number     VARCHAR(10) NOT NULL,
  type            ENUM('STANDARD','DELUXE','FAMILY_SUITE','PRESIDENTIAL') NOT NULL,
  status          ENUM('AVAILABLE','BOOKED','OCCUPIED','UNDER_MAINTENANCE') DEFAULT 'AVAILABLE',
  price_per_night DECIMAL(10,2) NOT NULL,
  max_occupancy   INT DEFAULT 2,
  UNIQUE (hotel_id, room_number),
  FOREIGN KEY (hotel_id) REFERENCES hotel(id),
  INDEX idx_room_type_status (hotel_id, type, status)
);

-- ════════════════ 4. BOOKING ════════════════
CREATE TABLE booking (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  guest_id     BIGINT NOT NULL,
  room_id      BIGINT NOT NULL,
  check_in     DATE NOT NULL,
  check_out    DATE NOT NULL,
  status       ENUM('CONFIRMED','CHECKED_IN','CHECKED_OUT','CANCELLED') DEFAULT 'CONFIRMED',
  total_amount DECIMAL(10,2) NOT NULL,
  created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (guest_id) REFERENCES user(id),
  FOREIGN KEY (room_id) REFERENCES room(id),
  CHECK (check_out > check_in),
  INDEX idx_room_dates (room_id, check_in, check_out),
  INDEX idx_guest (guest_id, status)
);

-- ════════════════ 5. PAYMENT ════════════════
CREATE TABLE payment (
  id         BIGINT AUTO_INCREMENT PRIMARY KEY,
  booking_id BIGINT NOT NULL UNIQUE,
  amount     DECIMAL(10,2) NOT NULL,
  mode       ENUM('CREDIT_CARD','CASH','CHEQUE') NOT NULL,
  status     ENUM('PENDING','COMPLETED','REFUNDED','FAILED') DEFAULT 'PENDING',
  txn_ref    VARCHAR(100),
  paid_at    DATETIME,
  FOREIGN KEY (booking_id) REFERENCES booking(id)
);

-- ════════════════ 6. SERVICE_CHARGE ════════════════
CREATE TABLE service_charge (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  booking_id  BIGINT NOT NULL,
  category    ENUM('FOOD','LAUNDRY','MINIBAR','SPA','AMENITY','OTHER'),
  description VARCHAR(300),
  amount      DECIMAL(10,2) NOT NULL,
  added_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (booking_id) REFERENCES booking(id),
  INDEX idx_booking (booking_id)
);

-- ════════════════ 7. HOUSEKEEPING_LOG ════════════════
CREATE TABLE housekeeping_log (
  id           BIGINT AUTO_INCREMENT PRIMARY KEY,
  room_id      BIGINT NOT NULL,
  staff_id     BIGINT NOT NULL,
  task_type    ENUM('CLEANING','LAUNDRY','DAMAGE_REPORT','RESTOCK') NOT NULL,
  notes        TEXT,
  performed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (room_id) REFERENCES room(id),
  FOREIGN KEY (staff_id) REFERENCES user(id),
  INDEX idx_room_date (room_id, performed_at)
);

-- ════════════════ 8. NOTIFICATION ════════════════
CREATE TABLE notification (
  id         BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id    BIGINT NOT NULL,
  booking_id BIGINT,
  channel    ENUM('EMAIL','SMS','PUSH') NOT NULL,
  message    TEXT,
  sent_at    DATETIME,
  status     ENUM('PENDING','SENT','FAILED') DEFAULT 'PENDING',
  FOREIGN KEY (user_id) REFERENCES user(id),
  FOREIGN KEY (booking_id) REFERENCES booking(id)
);
Why composite index idx_room_dates? The "is room available" query filters by room_id + date overlap. A single composite index on (room_id, check_in, check_out) makes overlap queries seek directly instead of scanning.
Step 7

Schema Reference — At a Glance

TablePurposeKey Columns
hotelProperty infoid, name, city, country
userGuests + staff (role)id, email, role
roomInventoryhotel_id, type, status, price
bookingReservation recordguest_id, room_id, check_in/out, status
paymentOne per bookingbooking_id, amount, mode, status
service_chargeAdd-ons: food, spa, etc.booking_id, category, amount
housekeeping_logTask audit per roomroom_id, staff_id, task_type
notificationOutbound messagesuser_id, booking_id, channel
Step 8

Key Flows — Sequence Diagrams

🔍 Search & Book Room

sequenceDiagram actor G as Guest participant API as BookingAPI participant SS as SearchService participant BS as BookingService participant DB as MySQL participant PG as PaymentGateway participant NS as NotificationService G->>API: GET /rooms?city=X&from&to&type API->>SS: searchAvailable(filter) SS->>DB: SELECT rooms LEFT JOIN booking
WHERE NOT overlaps(from,to) DB-->>SS: available rooms SS-->>G: list G->>API: POST /bookings {roomId,dates} API->>BS: createBooking() BS->>DB: BEGIN TXN BS->>DB: SELECT room FOR UPDATE BS->>DB: verify no overlap BS->>DB: INSERT booking (CONFIRMED) BS->>PG: chargeCard() PG-->>BS: txnRef BS->>DB: INSERT payment (COMPLETED) BS->>DB: COMMIT BS->>NS: sendConfirmationEmail() BS-->>G: 201 Booking

❌ Cancel Booking (24hr Rule)

sequenceDiagram actor G as Guest participant API participant BS as BookingService participant RS as RefundService participant DB G->>API: DELETE /bookings/{id} API->>BS: cancel(id) BS->>DB: SELECT booking alt checkIn - now > 24h BS->>RS: fullRefund() RS-->>BS: refunded else within 24h BS->>RS: partialRefund(0) RS-->>BS: no refund end BS->>DB: UPDATE booking SET status=CANCELLED BS-->>G: 200 OK (refund details)

🔔 Check-in Reminder (Scheduled Job)

sequenceDiagram participant CRON as Scheduler (daily 9AM) participant NS as NotificationService participant DB participant EM as Email/SMS Provider CRON->>NS: dispatchReminders() NS->>DB: SELECT bookings WHERE
check_in = TOMORROW AND status=CONFIRMED DB-->>NS: bookings[] loop each booking NS->>EM: send(user.email, template) NS->>DB: INSERT notification (SENT) end
Step 9

Design Patterns Used

🏭 Factory — RoomFactory

Instantiates correct Room subclass (Standard/Deluxe/Suite) based on type. Centralises construction logic and default pricing.

📣 Observer — Notification

BookingService publishes events (BookingCreated, CheckInTomorrow). NotificationService subscribes & fires email/SMS/push.

💳 Strategy — Payment

PaymentStrategy interface with CardStrategy, CashStrategy, ChequeStrategy. Swap implementations without touching booking logic.

🔄 State — Booking

Booking transitions CONFIRMED → CHECKED_IN → CHECKED_OUT. Each state restricts valid operations (no cancel after checkout).

🗄️ Repository / DAO

BookingRepository, RoomRepository. Clean separation between DB and business logic. Easy to mock in tests.

🧩 Singleton — Services

Stateless services (BookingService, NotificationService) as singletons wired by DI framework (Spring). One instance per JVM.

Step 10

BookingService — Core Logic

Transactional booking creation with pessimistic lock to prevent double-booking under concurrency.

java · BookingService
@Service
public class BookingService {
  private final BookingRepository bookingRepo;
  private final RoomRepository    roomRepo;
  private final PaymentService    paymentService;
  private final EventPublisher    publisher;

  @Transactional
  public Booking createBooking(BookingRequest req) {
    // 1. Lock the room row for this transaction
    Room room = roomRepo.findByIdForUpdate(req.getRoomId())
        .orElseThrow(() -> new RoomNotFoundException());

    // 2. Verify no overlap
    boolean clash = bookingRepo.existsOverlap(
        room.getId(), req.getCheckIn(), req.getCheckOut());
    if (clash) throw new RoomUnavailableException();

    // 3. Compute total
    BigDecimal nights = BigDecimal.valueOf(
        ChronoUnit.DAYS.between(req.getCheckIn(), req.getCheckOut()));
    BigDecimal total = room.getPricePerNight().multiply(nights);

    // 4. Persist booking
    Booking booking = Booking.builder()
        .guestId(req.getGuestId())
        .roomId(room.getId())
        .checkIn(req.getCheckIn())
        .checkOut(req.getCheckOut())
        .status(BookingStatus.CONFIRMED)
        .totalAmount(total)
        .build();
    booking = bookingRepo.save(booking);

    // 5. Charge & publish event (Observer fires notification)
    paymentService.charge(booking, req.getPaymentMode());
    publisher.publish(new BookingCreatedEvent(booking));
    return booking;
  }

  @Transactional
  public RefundResult cancel(Long bookingId) {
    Booking b = bookingRepo.findById(bookingId).orElseThrow();
    if (b.getStatus() != BookingStatus.CONFIRMED)
      throw new IllegalStateException("Cannot cancel");

    long hoursToCheckIn = ChronoUnit.HOURS.between(
        LocalDateTime.now(), b.getCheckIn().atStartOfDay());

    RefundResult refund = (hoursToCheckIn > 24)
        ? paymentService.fullRefund(b)
        : RefundResult.noRefund();

    b.setStatus(BookingStatus.CANCELLED);
    bookingRepo.save(b);
    publisher.publish(new BookingCancelledEvent(b, refund));
    return refund;
  }
}
Concurrency risk: Two guests booking the same room for overlapping dates at the exact same millisecond. Solution above: SELECT … FOR UPDATE on the room row inside the transaction → serialises conflicting bookings at DB level.
Step 11

Critical SQL Queries

sql · search available rooms
-- Rooms in Bangalore, Deluxe, Mar 1–5 — not overlapping any active booking
SELECT r.* FROM room r
  JOIN hotel h ON h.id = r.hotel_id
WHERE h.city = 'Bangalore'
  AND r.type = 'DELUXE'
  AND r.status = 'AVAILABLE'
  AND NOT EXISTS (
    SELECT 1 FROM booking b
    WHERE b.room_id = r.id
      AND b.status IN ('CONFIRMED','CHECKED_IN')
      AND b.check_in < '2026-03-05'
      AND b.check_out > '2026-03-01'
  );
sql · who booked room X?
SELECT u.name, u.email, b.check_in, b.check_out, b.status
FROM booking b
  JOIN user u ON u.id = b.guest_id
WHERE b.room_id = 42
ORDER BY b.check_in DESC;
sql · all rooms booked by a customer
SELECT r.room_number, h.name AS hotel, b.check_in, b.check_out
FROM booking b
  JOIN room r  ON r.id = b.room_id
  JOIN hotel h ON h.id = r.hotel_id
WHERE b.guest_id = 101
ORDER BY b.check_in DESC;
sql · total bill for a booking
SELECT
  b.total_amount AS room_charge,
  COALESCE(SUM(sc.amount), 0) AS services,
  b.total_amount + COALESCE(SUM(sc.amount), 0) AS grand_total
FROM booking b
  LEFT JOIN service_charge sc ON sc.booking_id = b.id
WHERE b.id = 555
GROUP BY b.id, b.total_amount;
Step 12

Interview Q&A

How do you prevent double-booking the same room?
Answer: Pessimistic lock — inside the booking transaction, do SELECT … FROM room WHERE id=? FOR UPDATE. Then check overlap. Conflicting transactions serialise at the DB. Alternative: optimistic lock with version column & retry, or Redis distributed lock for multi-node.
How do you model room availability efficiently?
Answer: Don't precompute per-date availability — too many rows. Instead derive it: "a room is free for [from, to] iff NO booking exists where check_in < to AND check_out > from". Composite index on (room_id, check_in, check_out) makes this a fast range seek.
How do you scale search?
Answer: Three layers — (1) Cache popular search results in Redis keyed by city|type|date-range, TTL 60s. (2) Move search to Elasticsearch for geo + filter queries. (3) Shard inventory by region if dataset gets huge.
How do you send check-in reminders?
Answer: Cron job runs daily at 9 AM → queries bookings where check_in = TOMORROW → pushes to Kafka → NotificationService consumes and dispatches email/SMS via provider. Writes to notification table for audit + retry.
Why is Payment 1-to-1 with Booking, not many?
Answer: A booking's payment is a single financial transaction (even refunds update status, not create a new row). If we needed split payments, we'd switch to 1-to-many. For this scale, 1-1 keeps invariants simple and makes the UNIQUE constraint enforce it at the DB level.
How would you support multiple hotels in different time zones?
Answer: Store all times as UTC DATETIME. Store the hotel's IANA timezone on the hotel row (e.g. Asia/Kolkata). Compute check-in/out at hotel-local midnight, convert to UTC for storage. Display always in hotel-local time.
How do you handle "no-show" guests?
Answer: A scheduled job at midnight after check-in date marks CONFIRMED bookings as NO_SHOW (add to enum). Charge full first night per policy. Room status becomes AVAILABLE again.
Refund policy enforcement — where does the 24-hour rule live?
Answer: In the service layer (BookingService.cancel()), not DB. Policies change — keep them in code (or config), not schema. The DB stores facts (amount refunded), the service enforces rules.
Wrap-up

Summary & Extensions

What we covered: 8 tables, 4 actors, 6 design patterns, search/book/cancel flows with sequence diagrams, concurrency story, and a dozen interview-ready answers.
Extensions to discuss: Dynamic pricing engine · Reviews / ratings · Group booking (N rooms) · Loyalty points · Channel-manager integration with MakeMyTrip / Booking.com · Multi-tenant SaaS model for hotel chains.
Trade-offs to flag: MySQL vs Postgres (we picked MySQL per spec — Postgres has richer range/exclusion constraints that would enforce "no overlapping bookings" declaratively). Sync vs async notification. Pessimistic vs optimistic locking.