Free preview

Why this matters: most candidates model this problem as Room, Booking, User boxes and then bolt a checkConflict loop somewhere. The design that scores is the one that starts from the hot path — is this room free for this interval? — and lets that question choose the data structure. Once bookings are ordered by start time, the conflict check collapses from a scan into a two-neighbor comparison, and everything else arranges itself around that move.

Start from the operations, not the nouns

book(roomId, interval, details)   -> Booking | ConflictError
suggest(interval, people, equip)  -> Room | none
cancel(bookingId)                 -> frees the slot immediately
isFree(roomId, interval)          -> the question under everything

Write these on the board before any class exists. Notice what they share: three of the four are, underneath, the same question — isFree. Direct booking asks it for one room; suggestion asks it for many; cancellation changes its future answers. That observation is the design: build one authoritative answerer of isFree, and make everything else a caller.

Booking is a value with sharp interval semantics

A Booking carries organizer, attendee count, equipment, and a half-open interval [start, end). Say the half-open part out loud when you introduce the type — it is the decision that makes back-to-back meetings legal, and it must be baked into the type's overlap logic, not sprinkled through call sites:

overlaps(a, b) = a.start < b.end AND b.start < a.end

One line, written once, on the interval type itself. If three different places each hand-roll their own <= versus < comparison, one of them will eventually disagree with the others, and you'll have a system where a slot looks free in search but conflicts on booking. Putting the comparison in exactly one place is separation of concerns at its smallest and most useful scale: interval math is the interval's job.

Per-room bookings, ordered by start time

Here is the central move. Each room owns its bookings in a structure sorted by start time — a TreeMap keyed by start, or a sorted set. Why ordering matters: with the interval semantics above, a candidate interval can only conflict with its immediate neighbors in start order — the booking that starts just before it (which might run into it) and the booking that starts just after it (which it might run into). Everything earlier has ended; everything later hasn't begun.

existing:   [9:00,10:00)   [10:30,11:00)   [14:00,15:00)
candidate:            [10:00, 10:45)
                       │
   floor  = [9:00,10:00)   -> ends 10:00, candidate starts 10:00 -> no clash
   ceiling= [10:30,11:00)  -> starts 10:30, candidate ends 10:45 -> CLASH

Two lookups, two comparisons, done — O(log n) against thousands of bookings, versus O(n) for the scan a flat list forces. The requirement conversation told us availability is the hot path; this is that fact turned into structure. If you take one sentence into the room, take this one: ordered by start time makes conflict a neighbor check.

This also gives each Room's calendar a crisp invariant to state: the structure never contains two overlapping intervals. Every insert is guarded by the neighbor check, so the invariant holds by construction — the system doesn't detect conflicts, it refuses to create them.

One availability authority

Wrap the neighbor check in a single RoomCalendar (one per room) with isFree(interval) and add(booking) — and make suggestion, direct booking, and any future caller go through it. This is the single-responsibility principle applied where it actually bites: the calendar's one job is to be right about time. The moment a second code path peeks at the raw booking structure "just to check quickly," you have two opinions about availability, and they will diverge.

Finding a room: filter is fixed, rank is policy

Suggestion is a pipeline: filter rooms by hard constraints (capacity ≥ attendee count, has required equipment, is free for the interval), then rank survivors and return the best. The two stages deserve different treatment, and saying why is worth points: the filter is requirements — it will not change, because a room that seats four cannot hold ten. The rank is preference — smallest-fit today, but nearest-to-team or energy-saving tomorrow. So the rank goes behind a small strategy interface and the filter stays as plain code. That's the open/closed principle deployed with judgment — a seam exactly where change is expected, and no seam where it isn't. A strategy interface for the capacity filter would be architecture cosplay.

Cancellation removes; it doesn't flag

Cancel looks up the booking, removes it from its room's ordered structure, done — the slot is genuinely gone from the data, so every future isFree is automatically correct. The tempting alternative — mark the booking CANCELLED and keep it in place — makes every conflict check carry a "skip cancelled" clause forever, and the first caller that forgets creates phantom conflicts. If you need cancellation history, keep it in a separate archive list; don't make the hot path pay for it.

The invariants, stated as a set

1. No two bookings in one room's calendar overlap
   (half-open semantics; enforced at insert, never re-checked later)
2. overlaps() is defined in exactly one place
3. Every availability answer comes from RoomCalendar — no second path
4. A cancelled booking is absent from the calendar, not flagged inside it
5. Suggestion never returns a room the filter would reject

What we rejected, and why

A global timeline of all bookings across rooms — makes "what's happening at 10:00 building-wide" easy, but the hot question is per-room, and a global structure forces every conflict check to filter by room first. Structure follows the query you actually run.

Free-slot tracking (store gaps, not bookings) — bookings become splits of a gap, cancellations become merges of neighbors. It can work, but you're maintaining the complement of your data, and every operation turns into interval surgery. Storing what exists is simpler than storing what doesn't.

Conflict check by scanning the day's bookings — correct, and fine at ten bookings. But you'd be building the slow version of a structure whose fast version costs nothing extra to write. When ordering is this cheap, take it.

Key takeaway

The design is four decisions, each named: half-open overlap logic written once on the interval type (separation of concerns); per-room bookings ordered by start so conflict is a floor/ceiling neighbor check, not a scan; one RoomCalendar as the sole availability authority (single responsibility); and room-finding split into a fixed filter and a swappable smallest-fit ranker (open/closed, applied only where change is expected). Cancellation removes from the structure — the invariant "no overlaps in the calendar" holds by construction.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue