# Low-Level Design: Hotel Management System — Room Booking, Check-In, and Billing

Source: https://www.techinterview.org/post/3233465935/lld-hotel-management/
Updated: 2026-04-17 · techinterview.org

## Core Entities

**Hotel**: hotel_id, name, address, star_rating, amenities (JSON array), check_in_time, check_out_time. **RoomType**: type_id, hotel_id, name (STANDARD, DELUXE, SUITE, PENTHOUSE), capacity (max guests), base_price, amenities. **Room**: room_id, hotel_id, type_id, number, floor, status (AVAILABLE, OCCUPIED, MAINTENANCE, HOUSEKEEPING), is_active. **Booking**: booking_id, hotel_id, guest_id, room_id (nullable until check-in), type_id, check_in_date, check_out_date, status (CONFIRMED, CHECKED_IN, CHECKED_OUT, CANCELLED, NO_SHOW), adults, children, total_amount, paid_amount, booking_source (DIRECT, OTA, WALK_IN), created_at. **Guest**: guest_id, first_name, last_name, email, phone, id_type, id_number, nationality, loyalty_tier (NONE, SILVER, GOLD, PLATINUM). **Invoice**: invoice_id, booking_id, line_items (JSON: [{desc, qty, rate, amount}]), subtotal, tax, total, issued_at.

## Room Availability and Booking


```
class BookingService:
    def get_available_rooms(self, hotel_id: int, check_in: date,
                             check_out: date, type_id: int,
                             adults: int) -> list[Room]:
        # Find rooms of requested type not booked in the date range
        booked_room_ids = self.db.query("""
            SELECT DISTINCT room_id FROM bookings
            WHERE hotel_id = :h AND room_id IS NOT NULL
              AND status IN ('CONFIRMED', 'CHECKED_IN')
              AND check_in_date  :in
        """, h=hotel_id, out=check_out, in=check_in)

        return self.db.query("""
            SELECT r.* FROM rooms r
            JOIN room_types rt ON r.type_id = rt.type_id
            WHERE r.hotel_id = :h AND r.type_id = :t
              AND r.status = 'AVAILABLE' AND r.is_active = TRUE
              AND r.room_id NOT IN :booked
              AND rt.capacity >= :adults
        """, h=hotel_id, t=type_id, booked=booked_room_ids, adults=adults)

    def create_booking(self, request: BookingRequest) -> Booking:
        with self.db.transaction():
            available = self.get_available_rooms(
                request.hotel_id, request.check_in,
                request.check_out, request.type_id, request.adults
            )
            if not available:
                raise NoRoomsAvailableError()

            booking = Booking(
                hotel_id=request.hotel_id,
                guest_id=request.guest_id,
                type_id=request.type_id,
                check_in_date=request.check_in,
                check_out_date=request.check_out,
                status=BookingStatus.CONFIRMED,
                total_amount=self._calculate_total(request)
            )
            self.repo.save(booking)
            return booking
```


## Dynamic Pricing (Revenue Management)

Hotel room prices are not static -- they vary by occupancy, season, day of week, and booking lead time. Pricing factors: base_price (from RoomType), occupancy multiplier: if >80% of rooms are booked for a night: apply a 1.2x multiplier. If <30%: apply a 0.9x discount. Day-of-week: weekends are 1.15x on leisure hotels, weekdays 1.15x on business hotels. Lead time: booking 90+ days in advance: 0.9x. Booking within 3 days: 1.3x (last-minute premium). Events: if a major conference is in the city on those dates: 1.5x. Pricing tiers are stored in a PricingRule table and evaluated at booking time. The calculated price is locked in at booking time and stored on the Booking -- future pricing changes don't affect confirmed bookings.

## Check-In, Housekeeping, and Billing

Check-in flow: (1) Guest arrives, receptionist looks up the booking by booking_id or guest name. (2) Verify guest ID (id_type, id_number). (3) Assign a specific room: pick from available AVAILABLE rooms of the booked type (prefer floors the guest has stayed on before, prefer non-adjacent to noisy rooms if available). (4) Update: booking.room_id = assigned_room, booking.status = CHECKED_IN, room.status = OCCUPIED. Housekeeping state machine: OCCUPIED → HOUSEKEEPING (after check-out or daily service request) → AVAILABLE (after housekeeping complete). The housekeeping app allows staff to mark rooms as cleaned. Room is not reassignable until status = AVAILABLE. Check-out and billing: (1) Retrieve all charges: base room rate (nightly * nights_stayed), additional charges (minibar, room service, spa -- added via the folio system during stay). (2) Apply loyalty discounts (GOLD: 5% off, PLATINUM: 10% off). (3) Generate invoice. (4) Process payment (charge to card on file). (5) Update room status to HOUSEKEEPING. Email invoice to guest. Folio charges: during the stay, any service charge (restaurant, spa) is added to the booking's folio (PostCharge: charge_id, booking_id, description, amount, posted_at, posted_by).

Asked at: [Airbnb Interview Guide](https://techinterview.org/airbnb-interview/)

Asked at: [Stripe Interview Guide](https://techinterview.org/stripe-interview/)

Asked at: [Shopify Interview Guide](https://techinterview.org/shopify-interview/)

Asked at: [Uber Interview Guide](https://techinterview.org/uber-interview/)
