The 100 problems, solved / Booking, allocation and simulators / 051 · Design a meeting room reservation system

Design a meeting room reservation system

Book, cancel and list with conflict detection, capacities and recurring meetings.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements and use cases

The prompt is short, so the first minute goes on pinning down what a booking is. The version below is the one most interviewers settle on once asked.

  • An organiser books a room for a half-open interval [start, end) with an expected head count, and gets a booking id back.
  • An organiser cancels a booking; the slot becomes free immediately.
  • Anyone lists the bookings of a room in a time window, in start order, a page at a time.
  • A booking is rejected if it overlaps an existing booking of the same room or if the head count exceeds the room capacity.
  • An organiser books a recurring meeting (weekly, n occurrences); every occurrence must be free or the whole request fails.

Out of scope: users and authentication, notifications, time zones (all times are UTC instants), and persistence. Non-functional constraints: a booking check on a room with thousands of existing bookings should be logarithmic, not linear; the rule set (capacity, business hours, maximum duration) will grow, so adding a rule must not touch the booking code; two concurrent requests for the same slot must never both succeed.

2. Core entities and relationships

  • Room: identity, capacity, and the calendar of its bookings. It owns the overlap check because it owns the data.
  • Booking: an immutable record of who booked which room for which interval; recurring bookings share a series_id.
  • TimeSlot: a value object [start, end) with an overlaps method, so interval arithmetic lives in one place.
  • BookingRule: an interface with one method, check(room, request), returning a reason string or None.
  • ReservationService: the orchestrator; it runs the rules, asks the room to insert, and hands out ids.
Entry pointCoreValues and rules1..*0..*0..*ReservationServicerooms: dict[str, Room]rules: list[BookingRule]lock: RLockbook(req) -> Bookingbook_series(req, every_seconds, count) -> listcancel(booking_id)list_bookings(room_id, window, page)Roomid: strcapacity: intcalendar: list[Booking]conflicts(slot) -> booladd(booking)remove(booking_id)Bookingid: strroom_id: strslot: TimeSlotorganiser: strattendees: intseries_id: str | NoneTimeSlotstart: intend: intoverlaps(other) -> boolshift(seconds) -> TimeSlotBookingRulecheck(room, req) -> str | NoneCapacityRuleBusinessHoursRule

Rooms compose bookings: a booking has no meaning outside its room, and deleting a room deletes its calendar. The service merely aggregates rules; rules are stateless objects that could be shared by several services, so the diamond is hollow there.

3. Interfaces and key methods

from dataclasses import dataclass
from typing import Optional, Protocol

@dataclass(frozen=True)
class TimeSlot:
    start: int  # unix seconds, inclusive
    end: int    # exclusive
    def overlaps(self, other: "TimeSlot") -> bool:
        """Half-open intervals overlap iff each starts before the other ends."""
        return self.start < other.end and other.start < self.end
    def shift(self, seconds: int) -> "TimeSlot":
        """A new slot of the same length, moved forward by seconds."""
        return TimeSlot(self.start + seconds, self.end + seconds)

@dataclass(frozen=True)
class BookingRequest:
    room_id: str
    slot: TimeSlot
    organiser: str
    attendees: int

class BookingRule(Protocol):
    def check(self, room: "Room", req: BookingRequest) -> Optional[str]:
        """Return a human-readable reason to reject, or None to allow."""

class ReservationService:
    def book(self, req: BookingRequest) -> "Booking":
        """Atomically validate and insert; raises BookingError with the first failing reason."""
    def book_series(self, req: BookingRequest, every_seconds: int, count: int) -> list:
        """All-or-nothing: either every occurrence is booked or none is."""
    def cancel(self, booking_id: str) -> None:
        """Idempotent: cancelling twice is not an error."""
    def list_bookings(self, room_id: str, window: TimeSlot, page: int = 0, size: int = 50) -> list:
        """Bookings overlapping the window, sorted by start, paged."""

Two invariants carry the design. A room's calendar is always sorted by start and pairwise non-overlapping, which is what makes the conflict check a binary search. And book is atomic: the rule checks, the overlap check and the insert happen under one lock, so no second caller can see the gap between "it was free" and "it is mine".

4. Design patterns and the choices behind them

Interval arithmetic as a value object

The force: overlap logic is easy to get wrong at the boundaries and is needed by the room, the recurring booking and the listing query. Putting it in an immutable value object means one implementation, one test, and the half-open rule (a meeting that ends at 10:00 does not clash with one starting at 10:00) is written once. The rejected alternative, comparing raw integers inside Room, duplicates the boundary condition in three places. See intervals and scheduling for why half-open is the right convention.

Rules as specifications

The force: capacity today, business hours tomorrow, "no meeting longer than four hours" next quarter. Each rule is a small object with a check method, collected in a list the service iterates, following the specification idea. Adding a rule is adding a class and one line of configuration; the booking code never changes, which is the open-closed principle in its plainest form. The rejected alternative is a growing if chain inside book, which is where the rules would otherwise end up.

Explicit failure reasons

The force: a rejected booking needs to say why. Rules return a reason string rather than a boolean, and the service wraps the first reason in a BookingError. The caller gets an exception at the boundary, never a silent None; that is the error-handling stance the method design should make obvious from the signatures.

5. Core flow and code

  1. The caller builds a BookingRequest and calls ReservationService.book.
  2. The service takes its lock and looks up the Room; an unknown room is a BookingError.
  3. Every BookingRule.check runs; the first non-empty reason aborts.
  4. The room runs conflicts(slot): a binary search on start times, then one overlap test against the neighbour on each side.
  5. If free, the room inserts the Booking at the found position, keeping the calendar sorted, and the service returns it.
import bisect, itertools, threading
from dataclasses import dataclass, field
from typing import Optional

class BookingError(Exception):
    pass

@dataclass(frozen=True)
class Booking:
    id: str
    room_id: str
    slot: TimeSlot
    organiser: str
    attendees: int
    series_id: Optional[str] = None

@dataclass
class Room:
    id: str
    capacity: int
    calendar: list = field(default_factory=list)  # Bookings sorted by slot.start, non-overlapping

    def _index(self, start: int) -> int:
        return bisect.bisect_left(self.calendar, start, key=lambda b: b.slot.start)

    def conflicts(self, slot: TimeSlot) -> bool:
        i = self._index(slot.start)
        before = self.calendar[i - 1] if i > 0 else None
        after = self.calendar[i] if i < len(self.calendar) else None
        return any(b is not None and b.slot.overlaps(slot) for b in (before, after))

    def add(self, booking: Booking) -> None:
        self.calendar.insert(self._index(booking.slot.start), booking)

    def remove(self, booking_id: str) -> None:
        self.calendar = [b for b in self.calendar if b.id != booking_id]

class CapacityRule:
    def check(self, room: Room, req: BookingRequest) -> Optional[str]:
        if req.attendees > room.capacity:
            return f"room {room.id} holds {room.capacity}, requested {req.attendees}"
        return None

class BusinessHoursRule:
    def __init__(self, open_hour: int = 8, close_hour: int = 20):
        self.open_hour, self.close_hour = open_hour, close_hour
    def check(self, room: Room, req: BookingRequest) -> Optional[str]:
        start_h = (req.slot.start // 3600) % 24
        end_h = ((req.slot.end - 1) // 3600) % 24
        spans_days = req.slot.start // 86400 != (req.slot.end - 1) // 86400
        if spans_days or start_h < self.open_hour or end_h >= self.close_hour:
            return "outside business hours"
        return None

class ReservationService:
    def __init__(self, rooms: list, rules: list):
        self.rooms = {r.id: r for r in rooms}
        self.rules = list(rules)
        self._lock = threading.RLock()
        self._ids = itertools.count(1)
        self._by_id: dict = {}

    def _validate(self, req: BookingRequest) -> Room:
        room = self.rooms.get(req.room_id)
        if room is None:
            raise BookingError(f"no room {req.room_id}")
        for rule in self.rules:
            reason = rule.check(room, req)
            if reason:
                raise BookingError(reason)
        if room.conflicts(req.slot):
            raise BookingError("slot already booked")
        return room

    def book(self, req: BookingRequest, series_id: Optional[str] = None) -> Booking:
        with self._lock:
            room = self._validate(req)
            booking = Booking(str(next(self._ids)), room.id, req.slot, req.organiser, req.attendees, series_id)
            room.add(booking)
            self._by_id[booking.id] = booking
            return booking

    def book_series(self, req: BookingRequest, every_seconds: int, count: int) -> list:
        if count < 1 or every_seconds < req.slot.end - req.slot.start:
            raise BookingError("a series needs at least one occurrence, none overlapping the next")
        with self._lock:
            reqs = [BookingRequest(req.room_id, req.slot.shift(k * every_seconds), req.organiser, req.attendees)
                    for k in range(count)]
            for r in reqs:            # validate all first: all-or-nothing
                self._validate(r)
            series = f"s{next(self._ids)}"
            return [self.book(r, series) for r in reqs]

    def cancel(self, booking_id: str) -> None:
        with self._lock:
            b = self._by_id.pop(booking_id, None)
            if b is not None:
                self.rooms[b.room_id].remove(booking_id)

    def list_bookings(self, room_id: str, window: TimeSlot, page: int = 0, size: int = 50) -> list:
        with self._lock:
            hits = [b for b in self.rooms[room_id].calendar if b.slot.overlaps(window)]
            return hits[page * size:(page + 1) * size]

The shift method on TimeSlot returns a new slot moved forward; it is the only piece of recurrence logic, which is deliberate. Weekly, daily and fortnightly series differ only in every_seconds, and the all-or-nothing check in book_series validates every occurrence before inserting any, so a clash on week six leaves weeks one to five untouched. Occurrences are checked against the calendar, not each other, so a step shorter than the meeting is refused up front.

6. Extensions, concurrency and what interviewers probe

  • "What if two people book the same slot at once?" The whole of book runs under one re-entrant lock, so the second caller re-runs conflicts after the first insert and is rejected. At larger scale the lock would be per room, which the design allows because a room owns its own calendar; see locks and mutexes.
  • "Add a maximum duration" or "no bookings on holidays". One more BookingRule class, registered in the list. The interviewer is checking that the booking method does not change.
  • "List bookings across all rooms for a person." Add a second index, by_organiser, kept in step inside book and cancel; the sorted calendar stays the source of truth.
  • "Recurring forever." Materialising occurrences does not work for an open-ended series; the answer is a RecurrenceRule expanded lazily for the window being queried, with exceptions stored as explicit cancellations, as in calendar conflict handling.
  • Test first: back-to-back slots do not conflict; a cancelled slot can be rebooked; a series with one clash books nothing.
  • Common mistake: checking overlap with a linear scan that compares against every booking, and using closed intervals so that adjacent meetings wrongly clash.

02Where this question was reported

  • Educative · Grokking LLD chapter 13
  • LLD Mastery · Google-tagged (Medium); conflict detection, recurring bookings; plus a 'list bookings' pagination variant
  • LLD Mastery · Google-tagged with Microsoft/Amazon/Meta; time-overlap detection
  • PracHub · PracHub OOD practice prompt
  • Codemia · Codemia Hard 'Calendar System'
  • thita.ai · thita.ai 'Hotel/Meeting Room Booking', advanced
  • GitHub · Machine coding 'Event Calendar' (Flipkart)