Design a Meeting Room Scheduler

Design the core of a workplace calendar. Employees book meetings with colleagues, and every meeting needs a physical room that is large enough and not already taken. Each person and each room has a calendar.

An organizer should be able to ask "when are these five people all free for 45 minutes tomorrow?", pick one of the answers and book it. The system must refuse a booking that clashes with anyone's existing meeting or with the room, and it must tell attendees when they are invited or a meeting is cancelled.

Two organizers may grab the last free room for the same hour at the same moment. In 45 minutes, model the entities, implement conflict detection and the free-slot search, and explain how room choice and notifications stay pluggable.

Use the LLD board for your class diagram and the LLD code tab to write and run your classes.

Design Path · Low-Level Design · problem 64 of 70

Ask before you design. Try answering each yourself, then compare.

  1. Do meetings that touch end-to-start count as a conflict?
    Typical answer

    No. A meeting ending at 10:00 and one starting at 10:00 can share a person or room.

  2. Who picks the room?
    Typical answer

    The system picks a free room with enough capacity; the rule for choosing among several should be easy to change.

  3. Do we handle recurring meetings and time zones?
    Typical answer

    Single occurrences in one time zone for the core; recurrence and zones are follow-ups.

  4. What should a conflict return?
    Typical answer

    A clear error naming who is busy, so the organizer can pick another time.

  5. How are attendees notified?
    Typical answer

    Through a notification interface; email or chat delivery is someone else's service.

  6. How big is a typical calendar?
    Typical answer

    Tens of meetings per person per week, a few hundred rooms per office. In-memory lists are fine for the exercise.

Write the overlap test for two half-open intervals first. Most bugs here are off-by-one at the edges.
People and rooms both need 'am I free at this time?'. Can they share one class for that?
To find common free time, what happens if you put everyone's busy intervals into one sorted list?
Checking that everyone is free and then writing to every calendar must not be interleaved with another booking.
Where would a 'prefer the room on my floor' rule plug in without editing the scheduler?

Requirements

Functional

  • Book a meeting for an organizer and invitees in a time slot, choosing a suitable free room.
  • Reject the booking and name the busy people if anyone clashes, or report that no room fits.
  • Find time slots of a given length when a set of people are all free within a window.
  • Cancel a meeting, freeing everyone's calendar and the room.
  • Notify attendees on invite and cancellation.

Non-functional

  • No double booking of a person or room, even with concurrent requests.
  • Booking and free-slot queries return in under 100 ms for 20 attendees with 50 meetings each.
  • New room-selection rules and notification channels can be added without changing the Scheduler.

Out of scope

  • Recurring meetings, time zones and working hours (discuss as extensions).
  • Accept and decline responses.
  • Video-call links and external calendar sync.

Class design

Time. TimeSlot is an immutable half-open interval with an overlaps test, so touching meetings never clash. Calendar keeps a sorted list of meetings and answers is_free and busy(window). Both User and Room own a calendar, which keeps availability logic in one place.

Booking flow. Scheduler.schedule takes a lock, checks every attendee's calendar and collects all busy names, asks the RoomPicker for a free room with enough capacity, then writes the Meeting into each attendee's and the room's calendar. Check and write happen under one lock, so two organizers can never both win the last room. Notifications are sent after the lock is released.

Free slots. Gather every attendee's busy intervals inside the window, sort them by start, and sweep with a cursor: any gap between the cursor and the next start that is at least the requested duration is a candidate. Overlapping busy intervals are merged by moving the cursor to the latest end seen.

Extensibility. Room choice is a strategy and notifications use observers; recurrence can expand into single occurrences that reuse the same conflict check.

Open the LLD board and press Reference to see the class diagram.

Patterns used

  • Strategy: RoomPicker decides which free room to use (smallest fit, nearest floor, has video) without touching Scheduler.
  • Observer: notifiers subscribe to the Scheduler and are told about invites and cancellations; adding chat alerts is one class.
  • Value object: TimeSlot is immutable and validated on creation, so no meeting can ever have an end before its start.

Reference implementation

import itertools
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, List, Optional


@dataclass(frozen=True, order=True)
class TimeSlot:
    start: datetime
    end: datetime

    def __post_init__(self):
        if self.end <= self.start:
            raise ValueError("a slot must end after it starts")

    def overlaps(self, other: "TimeSlot") -> bool:
        return self.start < other.end and other.start < self.end   # touching edges do not clash

    def __str__(self):
        return f"{self.start:%H:%M}-{self.end:%H:%M}"


@dataclass
class Meeting:
    meeting_id: int
    title: str
    organizer: str
    attendees: List[str]
    room: "Room"
    slot: TimeSlot


class Calendar:
    """Busy time for one person or room. Kept sorted by start time."""
    def __init__(self):
        self.meetings: List[Meeting] = []

    def is_free(self, slot: TimeSlot) -> bool:
        return not any(m.slot.overlaps(slot) for m in self.meetings)

    def add(self, meeting: Meeting):
        self.meetings.append(meeting)
        self.meetings.sort(key=lambda m: m.slot)

    def remove(self, meeting: Meeting):
        self.meetings.remove(meeting)

    def busy(self, window: TimeSlot) -> List[TimeSlot]:
        return [m.slot for m in self.meetings if m.slot.overlaps(window)]


@dataclass
class User:
    name: str
    calendar: Calendar = field(default_factory=Calendar)


@dataclass
class Room:
    name: str
    capacity: int
    calendar: Calendar = field(default_factory=Calendar)


class RoomPicker(ABC):
    @abstractmethod
    def pick(self, rooms: List[Room], headcount: int, slot: TimeSlot) -> Optional[Room]: ...


class SmallestFittingRoom(RoomPicker):
    """Leave big rooms free for big meetings."""
    def pick(self, rooms, headcount, slot):
        fits = [r for r in rooms if r.capacity >= headcount and r.calendar.is_free(slot)]
        return min(fits, key=lambda r: r.capacity, default=None)


class Notifier(ABC):
    @abstractmethod
    def notify(self, user: str, message: str): ...


class ConsoleNotifier(Notifier):
    def notify(self, user, message):
        print(f"  [notify {user}] {message}")


class ConflictError(Exception):
    pass


class Scheduler:
    def __init__(self, room_picker: RoomPicker):
        self.users: Dict[str, User] = {}
        self.rooms: List[Room] = []
        self.meetings: Dict[int, Meeting] = {}
        self.picker, self.listeners = room_picker, []
        self._ids = itertools.count(1)
        self._lock = threading.Lock()                 # conflict check + insert must be atomic

    def subscribe(self, notifier: Notifier):
        self.listeners.append(notifier)

    def _announce(self, meeting: Meeting, message: str):
        for person in meeting.attendees:
            for listener in self.listeners:
                listener.notify(person, message)

    def schedule(self, title: str, organizer: str, invitees: List[str], slot: TimeSlot) -> Meeting:
        people = [organizer] + [p for p in invitees if p != organizer]
        with self._lock:
            busy = [p for p in people if not self.users[p].calendar.is_free(slot)]
            if busy:
                raise ConflictError(f"busy at {slot}: {', '.join(busy)}")
            room = self.picker.pick(self.rooms, len(people), slot)
            if room is None:
                raise ConflictError(f"no room for {len(people)} people at {slot}")
            meeting = Meeting(next(self._ids), title, organizer, people, room, slot)
            for p in people:
                self.users[p].calendar.add(meeting)
            room.calendar.add(meeting)
            self.meetings[meeting.meeting_id] = meeting
        self._announce(meeting, f"invited: '{title}' {slot} in {room.name}")
        return meeting

    def cancel(self, meeting_id: int):
        with self._lock:
            meeting = self.meetings.pop(meeting_id)
            for p in meeting.attendees:
                self.users[p].calendar.remove(meeting)
            meeting.room.calendar.remove(meeting)
        self._announce(meeting, f"cancelled: '{meeting.title}' {meeting.slot}")

    def free_slots(self, people: List[str], window: TimeSlot, duration: timedelta) -> List[TimeSlot]:
        """Merge everyone's busy intervals, then return the gaps that are long enough."""
        busy = sorted(s for p in people for s in self.users[p].calendar.busy(window))
        gaps, cursor = [], window.start
        for s in busy:
            if s.start - cursor >= duration:
                gaps.append(TimeSlot(cursor, s.start))
            cursor = max(cursor, s.end)
        if window.end - cursor >= duration:
            gaps.append(TimeSlot(cursor, window.end))
        return gaps


if __name__ == "__main__":
    day = datetime(2026, 11, 2)
    at = lambda h, m=0: day.replace(hour=h, minute=m)
    s = Scheduler(SmallestFittingRoom())
    s.subscribe(ConsoleNotifier())
    for name in ("alice", "bob", "carol", "dan"):
        s.users[name] = User(name)
    s.rooms += [Room("Board Room", 12), Room("Huddle", 4)]

    print("bob books a 1:1 with dan")
    s.schedule("1:1", "bob", ["dan"], TimeSlot(at(10), at(11)))
    s.schedule("Lunch talk", "carol", [], TimeSlot(at(12), at(13)))
    m = s.schedule("Sync", "alice", ["carol"], TimeSlot(at(10, 15), at(10, 45)))
    print("Huddle is taken at 10:15, so the sync goes to:", m.room.name)
    try:
        s.schedule("Design review", "alice", ["bob", "carol"], TimeSlot(at(10, 30), at(11, 30)))
    except ConflictError as e:
        print("design review rejected ->", e)

    window, need = TimeSlot(at(9), at(14)), timedelta(minutes=45)
    options = s.free_slots(["alice", "bob", "carol"], window, need)
    print("free for alice, bob, carol (45 min):", ", ".join(map(str, options)))
    first = options[0]
    m = s.schedule("Design review", "alice", ["bob", "carol"], TimeSlot(first.start, first.start + need))
    print(f"booked #{m.meeting_id} in {m.room.name} at {m.slot}")
    print("cancelling the 1:1")
    s.cancel(1)
    print("bob's day now:", [f"{x.title} {x.slot}" for x in s.users["bob"].calendar.meetings])

Trade-offs

  • A single scheduler lock is simple and safe but serialises all bookings; per-calendar locks scale better but need ordered acquisition.
  • A sorted list per calendar is easy and fast for small calendars; an interval tree gives logarithmic queries for very busy rooms.
  • Synchronous notifications are simple but slow the caller; queuing them decouples delivery but delays invites slightly.
  • Picking the smallest fitting room saves big rooms but may place a team on another floor.

What a strong answer shows

  • Defines interval overlap correctly with half-open semantics.
  • Gives people and rooms the same calendar abstraction.
  • Makes the conflict check and the calendar writes atomic.
  • Finds common free time with a sort-and-sweep rather than checking every minute.
  • Keeps room selection and notification delivery pluggable.
  • Reports conflicts helpfully, naming who is busy.

Mistakes to avoid

  • Using <= in the overlap test so back-to-back meetings are rejected, or the reverse, letting real overlaps through.
  • Booking attendees one by one and failing halfway, leaving some calendars updated and others not.
  • Scanning the window minute by minute to find free time.
  • Sending notifications while holding the booking lock.
  • Hard-coding room choice and email sending inside the scheduler.

Follow-ups to expect

  • How would you support recurring meetings? (Store a rule, expand occurrences within a horizon, and check each for conflicts.)
  • How would you scale to a whole company? (Lock or version per calendar, and lock the attendee set in a fixed order to avoid deadlocks.)
  • How would free-slot search respect working hours and time zones? (Intersect the window with each person's working hours in UTC first.)
  • How would you make free-slot search also require a free room? (Intersect attendee gaps with each room's gaps and keep slots where any room fits.)

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url