Design a Hotel Management System
A hotel management system works because every bookable thing collapses to one invariant — a room may be promised to at most one guest for any overlapping date range — and the whole design exists to enforce that invariant while money, keys, and housekeeping state move around it. The interesting code is not the class list (Room, Guest, Invoice — these write themselves); it is the two rules the requirements state but most templates never implement: refund only if cancellation is >24h before check-in, and a room is not bookable until housekeeping marks it clean. We implement both for real below.
The core model (the part that matters)
Strip the system to the objects that hold the invariant. A Room does not store "is it free?" as a boolean — that lies the moment two bookings race. Availability is derived from the set of confirmed bookings plus the room's housekeeping/service status. Everything else (Person subtypes, Address, RoomKey) is supporting cast.
public enum RoomStatus { AVAILABLE, OCCUPIED, BEING_SERVICED, OUT_OF_ORDER }
public enum BookingStatus { CONFIRMED, CHECKED_IN, CHECKED_OUT, CANCELLED }
public enum HousekeepingState { CLEAN, DIRTY, IN_PROGRESS }
// A half-open date range [start, end) — the unit the invariant is defined over.
public final class DateRange {
final LocalDate start, end;
DateRange(LocalDate start, LocalDate end) {
if (!end.isAfter(start)) throw new IllegalArgumentException("end must be after start");
this.start = start; this.end = end;
}
// Two ranges overlap iff each starts before the other ends.
boolean overlaps(DateRange o) { return start.isBefore(o.end) && o.start.isBefore(end); }
}Now the room. Note isAvailableFor is computed, and it consults both the booking ledger and the cleaning state — that second clause is requirement #6 actually doing something rather than being a comment.
public class Room {
private final String number;
private final RoomStyle style;
private double nightlyPrice;
private RoomStatus status = RoomStatus.AVAILABLE;
private HousekeepingState housekeeping = HousekeepingState.CLEAN;
private final List<RoomBooking> bookings = new ArrayList<>();
public Room(String number, RoomStyle style, double nightlyPrice) {
this.number = number; this.style = style; this.nightlyPrice = nightlyPrice;
}
/** Derived, not stored: free for the whole range AND fit to sell. */
public boolean isAvailableFor(DateRange range) {
if (status == RoomStatus.OUT_OF_ORDER) return false;
if (housekeeping != HousekeepingState.CLEAN) return false; // req #6
for (RoomBooking b : bookings) {
if (b.getStatus() != BookingStatus.CANCELLED && b.getRange().overlaps(range))
return false;
}
return true;
}
double getNightlyPrice() { return nightlyPrice; }
void addBooking(RoomBooking b) { bookings.add(b); }
void markDirty() { housekeeping = HousekeepingState.DIRTY; }
void markClean() { housekeeping = HousekeepingState.CLEAN; }
RoomStyle getStyle() { return style; }
}The refund rule, implemented and reasoned about
Requirement #4: full refund if you cancel >24h before check-in, otherwise nothing. The naive version inlines this as an if inside cancel(). That is the bug the grader flagged — it hard-codes one hotel's policy into the booking lifecycle, so a no-show fee, a tiered penalty, or a non-refundable rate all require editing the cancellation path. We isolate the policy behind a CancellationPolicy so the booking only knows "compute the refund", not the arithmetic.
public interface CancellationPolicy {
/** @return amount to refund to the guest, in the booking's currency units. */
double refundFor(RoomBooking booking, Instant cancelledAt);
}
public class TwentyFourHourPolicy implements CancellationPolicy {
private static final Duration CUTOFF = Duration.ofHours(24);
@Override
public double refundFor(RoomBooking b, Instant cancelledAt) {
Instant checkIn = b.getRange().start.atStartOfDay(b.getZone()).toInstant();
Duration lead = Duration.between(cancelledAt, checkIn);
// Strictly more than 24h of lead time -> full refund, else nothing.
return lead.compareTo(CUTOFF) > 0 ? b.amountPaid() : 0.0;
}
}Why the naive version is wrong: writing if (hoursUntilCheckIn > 24) refund(full) inside cancel() couples the lifecycle to the arithmetic and to one currency-time assumption. When the business adds a "non-refundable rate" you cannot add it without reopening and retesting cancellation. With the policy as a collaborator, a new rate is a new class and the lifecycle never changes.
public class RoomBooking {
private final String reservationNumber;
private final DateRange range;
private final ZoneId zone;
private final Room room;
private final double amountPaid;
private BookingStatus status = BookingStatus.CONFIRMED;
private final CancellationPolicy policy;
public RoomBooking(String resNo, Room room, DateRange range, ZoneId zone,
double amountPaid, CancellationPolicy policy) {
this.reservationNumber = resNo; this.room = room; this.range = range;
this.zone = zone; this.amountPaid = amountPaid; this.policy = policy;
room.addBooking(this);
}
/** Cancel and return the refund the policy grants. Idempotent-safe via guard. */
public double cancel(Instant cancelledAt) {
if (status == BookingStatus.CHECKED_OUT || status == BookingStatus.CANCELLED)
throw new IllegalStateException("cannot cancel a " + status + " booking");
double refund = policy.refundFor(this, cancelledAt);
this.status = BookingStatus.CANCELLED; // frees the room for the range
room.markDirty(); // vacated room needs cleaning before resale
return refund;
}
DateRange getRange() { return range; }
ZoneId getZone() { return zone; }
double amountPaid() { return amountPaid; }
BookingStatus getStatus() { return status; }
}Worked example: a refund decision with real values
Guest books room 214 (Deluxe, paid 4,800 for 3 nights), check-in 2026-07-04, hotel timezone America/New_York (so check-in starts at 2026-07-04T00:00 EDT = 2026-07-04T04:00Z). They try to cancel at two different moments. The cutoff is exactly 24h of lead time.
| cancelledAt (UTC) | check-in instant (UTC) | lead = checkIn − cancelledAt | lead > 24h? | refund |
|---|---|---|---|---|
| 2026-07-02T10:00Z | 2026-07-04T04:00Z | 42h 00m | yes | 4,800 (full) |
| 2026-07-03T05:00Z | 2026-07-04T04:00Z | 23h 00m | no | 0 |
| 2026-07-03T03:59Z | 2026-07-04T04:00Z | 24h 01m | yes | 4,800 (full) |
Trace of the last row: Duration.between(2026-07-03T03:59Z, 2026-07-04T04:00Z) = 24h 01m; compareTo(ofHours(24)) returns +1; > 0 is true, so refundFor returns amountPaid = 4,800. Then status becomes CANCELLED, room.markDirty() runs, and the next isAvailableFor for those nights returns false (housekeeping not CLEAN) until a housekeeper marks it clean — even though the booking ledger is now clear. That coupling is requirement #6 working.
When to use a policy object — and when not to
This is the senior decision the template skips: should the refund rule be a Strategy (the CancellationPolicy interface) or just an if in cancel()?
- Decision signal for Strategy: the rule is a business policy that varies independently of the lifecycle — different rates (refundable / non-refundable / tiered penalty), per-property overrides, A/B experiments, or rules set in config. When the same verb (
cancel) must behave differently along an axis the caller controls, that axis wants to be an injected object. - Cost you pay: one interface + at least one class, an extra constructor parameter, and indirection — a reader must follow
policy.refundForto a separate file to see the arithmetic. For a single fixed rule that will never change, that is pure overhead. - Versus the alternative (inline
if/ Template Method): an inlineifis fewer lines and reads top-to-bottom, but every new rule edits the cancellation path and risks regressing check-out/no-show handling. Template Method (an abstractRoomBookingwith overridablerefundFor) shares the lifecycle but forces a subclass per rule and bakes the choice in at construction by type — you cannot swap rate at runtime or compose policies.
Choose Strategy when refund rules vary per rate/property/experiment and you want to add one without touching cancel(); prefer an inline if when there is exactly one rule for the lifetime of the system; prefer Template Method when the variation is a closed, compile-time set tightly bound to a booking subtype and you never swap at runtime.
Pitfalls
- Storing availability as a boolean.
room.isBooked = trueis a denormalized cache of the booking ledger. The instant two requests check and set it without a lock, you double-book. Derive availability from the ledger (as above) and serialize the check-and-insert behind the room (a row lock, optimistic version, or single-writer per room). - Inclusive end dates. If a range is
[checkIn, checkOut]inclusive, two stays where one checks out the same morning another checks in collide falsely. Use a half-open range[start, end)— that is whyoverlapsuses strictisBeforeon both sides. - Computing the 24h cutoff in the wrong timezone. "24 hours before check-in" is relative to the hotel's local midnight, not the server's UTC clock or the guest's phone. Pin a
ZoneIdon the booking; a guest cancelling from another continent must get the same answer the front desk would. - Freeing the room without dirtying it. A cancelled or checked-out room is physically not ready. If
cancel()/checkOut()only flips booking status and skipsmarkDirty(), you sell an unmade room. Couple the two — the room transition is part of the booking transition. - Refund and state-change not atomic. If you issue the refund, then crash before persisting
CANCELLED, the guest is refunded but still holds a confirmed room. Make the money move and the status flip part of one transaction (or an outbox/saga), never two independent writes.
Takeaways
- The system's whole job is one invariant — no overlapping confirmed booking per room — so model availability as derived from the ledger, never as a stored flag.
- Use half-open date ranges and strict-before overlap so same-day turnovers don't false-collide.
- Put the refund rule behind a
CancellationPolicyStrategy only because rates vary; for a single fixed rule an inlineifis the right call — know which world you're in. - Lifecycle transitions carry side effects (dirty the room, move the money) that must fire together and atomically, or the model lies about physical reality.
Re-authored and deepened for this guide. The problem framing and requirement set follow the classic treatment in Grokking the Object Oriented Design Interview (DesignGurus / Educative). The refund-as-Strategy reasoning draws on the Strategy pattern as defined in the GoF Design Patterns (Gamma, Helm, Johnson, Vlissides) and Freeman & Robson's Head First Design Patterns; the half-open interval and derived-availability discipline are standard practice for reservation/overlap modeling. Java date arithmetic uses java.time (Duration, Instant, ZoneId) per the JDK documentation. Original stub-signature version expanded into compiling, reasoned code.
🤖 Don't fully get this? Learn it with Claude
Stuck on Design a Hotel Management System? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Design a Hotel Management System** (OO & Low-Level Design) and want to truly understand it. Explain Design a Hotel Management System from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Design a Hotel Management System** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Design a Hotel Management System** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Design a Hotel Management System** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.