CMD Guide
HomeOO & Low-Level DesignOO Design Problems

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 − cancelledAtlead > 24h?refund
2026-07-02T10:00Z2026-07-04T04:00Z42h 00myes4,800 (full)
2026-07-03T05:00Z2026-07-04T04:00Z23h 00mno0
2026-07-03T03:59Z2026-07-04T04:00Z24h 01myes4,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.

diagram
diagram

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()?

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

Takeaways


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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes