Hotel Management#
Problem statement (interviewer prompt)
Design a hotel management system: rooms with categories + pricing, search by date range + occupancy, reservation with deposit, check-in / check-out, billing for stay + extras (mini-bar / room service), housekeeping status, and analytics for the operator.
classDiagram
class Hotel
class Room
class Reservation
class Guest
class Payment
Hotel "1" *-- "many" Room
Guest "1" -- "many" Reservation
Reservation --> Room
Reservation --> Payment
classDiagram
direction TB
class Hotel {
+id
+name
+location
+rooms: List~Room~
+pricing: PricingPolicy
}
class Room {
+id
+number
+type: RoomType
+rate
+amenities
+status: RoomState
}
class RoomState {
<<enumeration>>
AVAILABLE
OCCUPIED
DIRTY
OUT_OF_SERVICE
}
class RoomType {
<<enumeration>>
STD
DELUXE
SUITE
}
class Guest {
+id
+name
+contact
+loyalty
}
class Reservation {
+id
+guest
+room: Room
+checkin
+checkout
+status: ResState
+totals
+idempotencyKey
}
class ResState {
<<enumeration>>
PENDING
CONFIRMED
CHECKED_IN
CHECKED_OUT
CANCELLED
}
class PricingPolicy {
<<interface>>
+rate(room, dates) Money
}
class FlatNight
class DynamicPricing
class PaymentService
class HousekeepingService
class ConciergeService
Hotel *-- Room
Hotel --> PricingPolicy
Guest "1" -- "many" Reservation
Reservation --> Room
Reservation --> ResState
Hotel --> PaymentService
Hotel --> HousekeepingService
Hotel --> ConciergeService
PricingPolicy <|.. FlatNight
PricingPolicy <|.. DynamicPricing
Room --> RoomState
Room --> RoomType
Detailed class model with multiplicities#
The expanded model below adds payment, housekeeping, and folio aggregates with explicit cardinalities. Multiplicities make the inventory math obvious: a Hotel owns many Rooms; each Room is reserved zero or more times across distinct date windows; each Reservation carries exactly one Guest, one Payment outcome, and many FolioLine charges accumulated during the stay.
classDiagram
direction TB
class Hotel {
+id
+name
+rooms: List~Room~
+pricing: PricingPolicy
}
class Room {
+id
+number
+floor
+type: RoomType
+baseRate
+status: RoomState
}
class RoomType {
<<enumeration>>
SINGLE
DOUBLE
SUITE
}
class RoomState {
<<enumeration>>
AVAILABLE
OCCUPIED
DIRTY
OUT_OF_SERVICE
}
class Reservation {
+id
+checkin
+checkout
+nights
+state: ResState
+total: Money
}
class ResState {
<<enumeration>>
REQUESTED
CONFIRMED
CHECKED_IN
CHECKED_OUT
CANCELLED
}
class Guest {
+id
+name
+contact
+loyaltyTier
}
class Payment {
+id
+amount: Money
+method
+status
+authorizedAt
}
class FolioLine {
+id
+description
+amount: Money
+postedAt
}
class HousekeepingTask {
+id
+room: Room
+assignedTo
+status
}
class Notifier {
<<interface>>
+notify(event)
}
Hotel "1" *-- "many" Room
Hotel "1" o-- "many" Reservation
Reservation "many" --> "1" Guest
Reservation "many" --> "1" Room
Reservation "1" --> "1" Payment
Reservation "1" *-- "many" FolioLine
Room "1" -- "0..1" HousekeepingTask
Hotel ..> Notifier
Room --> RoomType
Room --> RoomState
Reservation --> ResState
Reservation aggregates FolioLine items (mini-bar, room service, taxes) so the bill is a sum at check-out; Payment is a separate aggregate because the authorisation and capture life cycle is different from the stay lifecycle. HousekeepingTask is loosely coupled to Room so a single room can be in service, awaiting cleaning, and assigned to a housekeeper without bloating the Room entity.
Reservation flow in Java#
The flow below shows the three-step booking critical path: checkAvailability filters rooms by date overlap, reserveRoom takes a row-level lock on the inventory row and inserts a tentative reservation, and confirmReservation flips state from REQUESTED to CONFIRMED only after payment authorisation. The lock window is tight; everything outside the database transaction runs without holding the lock.
public class ReservationService {
private final RoomRepository rooms;
private final ReservationRepository reservations;
private final PaymentGateway payments;
private final EventBus bus;
public List<Room> checkAvailability(RoomType type,
LocalDate in, LocalDate out, int guests) {
return rooms.findAvailable(type, in, out, guests);
}
@Transactional(isolation = SERIALIZABLE)
public Reservation reserveRoom(String guestId, long roomId,
LocalDate in, LocalDate out,
String idemKey) {
Reservation prior = reservations.findByIdempotencyKey(idemKey);
if (prior != null) return prior;
Room room = rooms.lockForUpdate(roomId);
if (reservations.overlapsWindow(roomId, in, out)) {
throw new RoomUnavailable(roomId, in, out);
}
Reservation r = new Reservation(guestId, room, in, out);
r.setState(ResState.REQUESTED);
r.setIdempotencyKey(idemKey);
r.setTotal(room.quote(in, out));
return reservations.save(r);
}
public Reservation confirmReservation(long reservationId, PaymentRequest pr) {
Reservation r = reservations.findById(reservationId);
PaymentResult auth = payments.authorize(pr, r.getTotal());
if (!auth.ok()) {
r.setState(ResState.CANCELLED);
reservations.save(r);
throw new PaymentFailed(auth.code());
}
r.attachPayment(auth.paymentId());
r.setState(ResState.CONFIRMED);
reservations.save(r);
bus.publish(new ReservationConfirmed(r.getId(), r.getGuestId()));
return r;
}
}
SERIALIZABLE isolation plus SELECT ... FOR UPDATE on the room row removes the double-book race at the cost of throughput; the idempotency-key check makes retries safe so a network blip cannot accidentally create two reservations.
Design patterns used#
The hotel domain is a textbook fit for four classical patterns, each isolating a dimension that varies independently.
- Factory (RoomType subtypes): a
RoomFactory.create(type)returns the rightRoomsubclass or pre-configures the right amenity set. Hides the conditional that would otherwise leak into reservation, billing, and housekeeping code paths whenever a new category (Family, Accessible, Penthouse) is added. - Strategy (PricingPolicy):
FlatNight,DynamicPricing,LoyaltyDiscount, andWeekendSurchargeall implementPricingPolicy.rate(room, dates). The hotel picks one at runtime; the booking flow never branches on the strategy in use. Swappable per market without recompile. - State (Reservation lifecycle):
ResStateformalises the transitionsREQUESTED -> CONFIRMED -> CHECKED_IN -> CHECKED_OUTwithCANCELLEDreachable from the first two. Each state exposes only legal actions: you cannot check in aCANCELLEDreservation or cancel one alreadyCHECKED_OUT. The state object owns the guard logic, not scatteredifstatements. - Observer (notifications):
ReservationConfirmed,CheckedIn, andCheckedOutevents fan out through anEventBusto subscribers likeEmailNotifier,SmsNotifier,HousekeepingScheduler, andLoyaltyAccrualService. Adding a new side effect (PDF invoice, partner CRM push) costs one new subscriber, no edits to the reservation service.
Common variants & follow-ups#
Interviewers usually push beyond the single-property baseline to test how the design scales and which decisions you would defer. The variants below show up in real loops.
- Multi-property chain: introduce a
Brandaggregate aboveHotel; loyalty points and corporate rates roll up across properties; cross-property availability search hits a federated inventory index. Discuss whether bookings are owned by the property or the brand. - Dynamic pricing: replace
FlatNightwithDynamicPricingthat consumes occupancy forecast, event-calendar boosts, and competitor scrape data. Cache quotes for 60 to 300 seconds so a search session sees stable prices. - Overbooking strategy: book to 105 to 115 percent of inventory based on historical no-show rate; on an actual overbook, walk the guest to a partner hotel at company expense and credit loyalty points. Discuss the legal and reputational tradeoff.
- Cancellation policy variants: free cancel until 48 hours before check-in, half refund inside the window, no refund on no-show. Encoded as a
CancellationPolicystrategy attached to the rate plan, not hard-coded. - Partial-room reservation (event venue): a ballroom is sold in time slots, not nights. Model
Bookableas a generic interface;RoomandVenueboth implement it; reservation windows are intervals on a calendar rather than whole nights. - Group bookings: one
GroupReservationaggregates many child reservations; rooming list is uploaded as a CSV; cancellation propagates with a configurable policy (cancel-all vs cancel-individual).
Edge cases#
These are the failure modes interviewers reliably probe; each one tests a different concurrency or domain invariant.
- Double-booking race: two clients call
reserveRoomfor the same room and the same night. Pessimistic locking (SELECT ... FOR UPDATEon the room row inside a serializable transaction) blocks one until the other commits; the loser seesRoomUnavailable. Optimistic locking (aversioncolumn) is cheaper but the loser must retry; pick pessimistic for tight inventory and optimistic for low-contention cases. - Partial cancellation: a 3-night stay where the guest leaves after night 2. Cancel future folio lines, charge the early-departure fee per policy, recompute taxes on the shortened stay, and notify housekeeping that the room is available a day early.
- No-show vs late check-in: at midnight on the check-in date with no arrival, the reservation is marked
NO_SHOWand the first night is charged; if the policy allows late arrival, alate-arrivalflag suppresses the cron job that would otherwise release the room. - Room downgrade when type unavailable: if the requested
SUITEis unavailable at check-in (maintenance, prior guest extended stay), offer aDELUXEat the suite rate or refund the difference. Encoded as aDowngradePolicy; never silently change the room type on the reservation row. - Payment failure mid-reservation: authorisation succeeded but capture fails at check-out. Move the reservation to a
PAYMENT_PENDINGstate; retry capture with exponential backoff; if all retries fail, escalate to manual collection and keep the room out of inventory until resolved so no follow-up guest is affected by an unresolved chargeback dispute.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
HLD |
Observability | metrics, logs, traces, SLOs | observability |
Quick reference#
Functional#
- Manage rooms, reservations, guests.
- Check-in / check-out / housekeeping.
- Dynamic pricing.
- Loyalty programs.
- Payments + invoicing.
Patterns#
- State (room), Strategy (pricing), Repository, Saga.
Refs#
- Hospitality industry whitepapers; Opera PMS docs.
- See also
ecommerce-marketplaces/airbnbfor the full-system view.
FAQ#
How do you search rooms by date range and occupancy?#
Index rooms by category and capacity, then query the Reservation table for overlapping date ranges; rooms with no overlap in the requested window are available.
How do you prevent double booking of the same room?#
Use a unique constraint on (room_id, date) or wrap the availability check and insert in a serializable transaction so two concurrent reservations cannot both succeed.
How is housekeeping status tracked?#
Each room carries a status enum like Clean, Dirty, OutOfOrder, Inspected. Check-out flips the room to Dirty and housekeeping updates it back to Clean after servicing.
What classes form the core of a hotel LLD?#
Hotel, Room, RoomCategory, Reservation, Guest, Payment, and HousekeepingTask cover the booking domain. Staff and Shift extend it for operational concerns.
How do you bill mini-bar and extras?#
Each charge is a folio line attached to the active Reservation; settle the folio total at check-out by summing line items plus the room rate per night.