Design a Meeting Scheduler / Calendar

Interval management as LLD: detect conflicts, assign the minimum number of rooms with a sweep-line heap, and extend to recurring events, attendee free-slot intersection, and concurrent double-booking. Maps to LeetCode 253 / My Calendar.

LLDoopintervalsheap

The real-world analogy: conference room clipboard timelines

Imagine walking down the hall of an office building. Outside each conference room door hangs a paper clipboard showing a calendar timeline:

  • Linear Scan vs. Sorted Registry: If the clipboard contains a disorganized stack of loose post-it notes with meeting times, you have to read every single note (taking O(N) operations) to check if your new meeting overlaps with any of them. If the clipboard has one single paper sheet pre-ruled with hours sorted from 9:00 AM to 5:00 PM, you immediately slide your eyes to your start time and look only at the slot immediately before and after (taking O(1) focus operations).
  • Double-booking Prevention (Lock): Two people walk up to the clipboard simultaneously, pencils in hand. They both notice the 2:00 PM slot is empty. If they both start writing without acknowledging the other, they will overwrite each other's text. To prevent this, they must agree that only one person holds the clipboard pencil at a time (Mutex Lock).

In software, we model this by sorting room timelines inside self-balancing binary search trees (like TreeMap or std::map) to perform O(log N) conflict lookups, and applying locks to prevent concurrent race conditions.


The ask

Book meetings with a start and end time across a set of rooms: reject overlaps in the same room, assign a free room when one's available, and answer "how many rooms do we need?" (This is My Calendar I/II and Meeting Rooms II / LeetCode 253 as a design problem.)


UML Class Diagram


The two core algorithms

  • Conflict detection (one room): two intervals [s1,e1) and [s2,e2) overlap iff s1 < e2 and s2 < e1. Keep each room's bookings sorted (a TreeMap/balanced BST keyed on start) so you can binary-search the neighbours and check only those — O(log n) per booking instead of scanning all.
  • Minimum rooms (sweep line + heap): sort meetings by start; keep a min-heap of end times of in-progress meetings. For each meeting, if the earliest end ≤ its start, a room freed up (pop and reuse); else allocate a new room (push). The heap's max size is the answer.

Think it through like the interview

Think it through: Meeting SchedulerLLD / Medium — LeetCode 253 + My Calendar0/3 stages

PROBLEMBook meetings across rooms with no overlaps; assign a free room; report the minimum rooms needed.

  1. 1

    Spec → entities

    What are the objects and what does each own?

  2. 2

    Conflict check without scanning everything

    How do I know a new [s,e) fits a room in better than O(n)?

    unlocks after the stage above
  3. 3

    Minimum rooms = max concurrency

    How many rooms do N meetings need?

    unlocks after the stage above

Implementation

Below are complete implementations with sorted interval lookups and concurrency protection locks.

Python

Python
import bisect
import threading
from typing import List, Tuple, Optional

class Meeting:
    def __init__(self, start: int, end: int, title: str):
        self.start = start
        self.end = end
        self.title = title

class Room:
    def __init__(self, room_id: str):
        self.room_id = room_id
        self.bookings: List[Tuple[int, int, Meeting]] = []  # Keep sorted by start time
        self.lock = threading.Lock()

    def book(self, meeting: Meeting) -> bool:
        with self.lock:
            # Binary search neighbor index using bisect
            idx = bisect.bisect_left(self.bookings, (meeting.start, meeting.end))
            
            # Check overlap with previous booking (if any)
            if idx > 0:
                prev_start, prev_end, _ = self.bookings[idx - 1]
                if prev_end > meeting.start:
                    return False  # Overlap!
                    
            # Check overlap with next booking (if any)
            if idx < len(self.bookings):
                next_start, next_end, _ = self.bookings[idx]
                if meeting.end > next_start:
                    return False  # Overlap!
                    
            # Insert into sorted list
            self.bookings.insert(idx, (meeting.start, meeting.end, meeting))
            return True

class Scheduler:
    def __init__(self):
        self.rooms: List[Room] = []
        self.lock = threading.Lock()

    def add_room(self, room_id: str):
        with self.lock:
            self.rooms.append(Room(room_id))

    def book_meeting(self, meeting: Meeting) -> Optional[str]:
        # Try booking in rooms
        for room in self.rooms:
            if room.book(meeting):
                return room.room_id
        return None

Java

Java
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

class Meeting {
    int start;
    int end;
    String title;
    Meeting(int s, int e, String t) {
        this.start = s;
        this.end = e;
        this.title = t;
    }
}

class Room {
    String roomId;
    // TreeMap keeps entries sorted by start time, allowing ceiling/floor log(N) operations
    private final TreeMap<Integer, Meeting> bookings = new TreeMap<>();
    private final ReentrantLock lock = new ReentrantLock();

    Room(String id) { this.roomId = id; }

    public boolean book(Meeting meeting) {
        lock.lock();
        try {
            // Find booking starting immediately at or after meeting.start
            Integer nextStart = bookings.ceilingKey(meeting.start);
            if (nextStart != null && nextStart < meeting.end) {
                return false; // Overlaps with next meeting
            }
            // Find booking starting before meeting.start
            Integer prevStart = bookings.floorKey(meeting.start);
            if (prevStart != null) {
                Meeting prevMeeting = bookings.get(prevStart);
                if (prevMeeting.end > meeting.start) {
                    return false; // Overlaps with previous meeting
                }
            }
            bookings.put(meeting.start, meeting);
            return true;
        } finally {
            lock.unlock();
        }
    }
}

public class Scheduler {
    private final List<Room> rooms = new ArrayList<>();
    private final ReentrantLock lock = new ReentrantLock();

    public void addRoom(String roomId) {
        lock.lock();
        try {
            rooms.add(new Room(roomId));
        } finally {
            lock.unlock();
        }
    }

    public String bookMeeting(Meeting meeting) {
        for (Room room : rooms) {
            if (room.book(meeting)) {
                return room.roomId;
            }
        }
        return null; // No available room fits this interval
    }
}

C++

C++
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <mutex>
#include <memory>

class Meeting {
public:
    int start;
    int end;
    std::string title;
    Meeting(int s, int e, std::string t) : start(s), end(e), title(t) {}
};

class Room {
private:
    std::string roomId;
    // Map in C++ is a red-black tree, sorted by key (start time)
    std::map<int, std::shared_ptr<Meeting>> bookings;
    std::mutex mtx;
public:
    Room(std::string id) : roomId(id) {}

    std::string getRoomId() const { return roomId; }

    bool book(std::shared_ptr<Meeting> meeting) {
        std::lock_guard<std::mutex> lock(mtx);
        
        // Find iterator pointing to first element >= meeting->start
        auto it = bookings.lower_bound(meeting->start);
        if (it != bookings.end() && it->first < meeting->end) {
            return false; // Overlap with next meeting
        }
        if (it != bookings.begin()) {
            auto prevIt = std::prev(it);
            if (prevIt->second->end > meeting->start) {
                return false; // Overlap with previous meeting
            }
        }
        
        bookings[meeting->start] = meeting;
        return true;
    }
};

class Scheduler {
private:
    std::vector<std::shared_ptr<Room>> rooms;
    std::mutex mtx;
public:
    void addRoom(const std::string& id) {
        std::lock_guard<std::mutex> lock(mtx);
        rooms.push_back(std::make_shared<Room>(id));
    }

    std::string bookMeeting(std::shared_ptr<Meeting> meeting) {
        for (auto& room : rooms) {
            if (room->book(meeting)) {
                return room->getRoomId();
            }
        }
        return "";
    }
};

TypeScript

type Meeting = { start: number; end: number };

class Scheduler {
  // Each room holds bookings sorted by start time.
  private rooms: Meeting[][] = [];

  private overlaps(a: Meeting, b: Meeting) {
    return a.start < b.end && b.start < a.end;   // half-open intervals
  }

  // Book into the first room with no conflict; open a new room if none fits.
  book(m: Meeting): number {
    for (let i = 0; i < this.rooms.length; i++) {
      if (!this.rooms[i].some((b) => this.overlaps(b, m))) {
        this.rooms[i].push(m);
        return i;                                 // assigned room index
      }
    }
    this.rooms.push([m]);                          // all busy → new room
    return this.rooms.length - 1;
  }
}
Half-open intervals remove the off-by-one

Treat times as [start, end). A meeting ending at 10:00 and one starting at 10:00 then don't conflict (s1 < e2 and s2 < e1 is false), which matches how calendars actually behave. State the convention up front — it kills a whole class of boundary bugs.


Interactive Quiz

Check yourself0/3 answered

1.

2.

3.


Complexity & follow-ups

  • Booking: O(log n) per room with a TreeMap; the naive some() scan above is O(n) per room — fine to start, then optimize when asked.
  • Min rooms: O(n log n) via the heap.
  • Recurring meetings: store a rule (RRULE) + expand lazily into instances within a query window — don't materialize infinite occurrences.
  • Time zones: store everything in UTC, convert at the edges; mind DST transitions.
  • Find a common slot across attendees: merge each person's busy intervals, then intersect the free gaps (interval intersection).
  • Concurrency / double-booking: two requests for the same room+time race — guard with a per-room lock or an optimistic check-and-insert (unique constraint on room+overlap) so the second booking fails cleanly.

Design drills

Design drills: Meeting Scheduler0/3 done

Whiteboard each one out loud for 5–10 minutes before you reveal what a strong answer covers — the gap between your sketch and the checklist is your study list. Progress is saved on this device.

Core

Find the earliest common free slot of duration D across K attendees.

Stretch

Two users book the same room for the same time simultaneously. Prevent the double-booking.

Core

Support recurring meetings (every weekday for a quarter) without storing infinite rows.