Design a Meeting Scheduler
Book rooms and time slots with overlap detection — classic Google, Microsoft, and Amazon LLD for calendar APIs and conflict resolution.
Model User, Room, Meeting, and TimeSlot. SchedulerService validates no overlapping bookings for room and attendees, then persists the meeting. Core trick: `TimeSlot.overlaps(other)` for interval intersection.
The interview question
“Design a meeting scheduler.” Users book a room (or virtual slot) for a start/end time with a list of attendees. Reject if the room or any participant is already busy.
Tests interval overlap logic, clean domain modeling, and a small service layer — not distributed calendar sync.
Clarifying questions (ask before coding)
cancel(meetingId) and optional reschedule — shows lifecycle thinking.attendees.size() <= room.capacity on book.Instant or UTC; v1 assume single TZ to save time.Requirements & assumptions (state aloud)
- schedule(user, room, slot, attendees) → Meeting or throw if conflict.
- TimeSlot: half-open or closed interval; consistent overlap rule everywhere.
- Conflict: same room double-booked OR any attendee has overlapping meeting.
- cancel(meetingId): remove from room and user calendars.
- v1: in-memory maps; no persistence, notifications, or video links.
High-level design
User holds a list of scheduled Meeting references (or IDs). Room holds its bookings. SchedulerService is the single entry point.
On schedule: build TimeSlot, verify room free, verify each attendee free, check capacity, then commit atomically (synchronized block in v1).
TimeSlot— start/end +overlaps()Meeting— id, organizer, room, slot, attendeesSchedulerService— schedule / cancel / list
UML class diagram (optional)
Draw SchedulerService at center with associations to Room and User — under 5 minutes on whiteboard.
Design patterns used (and why)
| Pattern | Where | Why (interview soundbite) |
|---|---|---|
| 🏛️Facade | SchedulerService | One API hides room lookup, overlap checks, and calendar updates. |
| 📦Value object | TimeSlot | Immutable interval; overlap logic lives in one place. |
| 🗃️Repository (light) | Maps for users/rooms/meetings | Swap in DB later without changing service contract. |
Full solution — domain model
import java.time.LocalDateTime;
import java.util.*;
public record TimeSlot(LocalDateTime start, LocalDateTime end) {
// Validates start is strictly before end at construction
public TimeSlot {
if (!start.isBefore(end))
throw new IllegalArgumentException("start must be before end");
}
// True if this interval intersects other (inclusive boundaries)
public boolean overlaps(TimeSlot other) {
return this.start.isBefore(other.end) && other.start.isBefore(this.end);
}
}
public class Meeting {
private final String id;
private final User organizer;
private final Room room;
private final TimeSlot slot;
private final List<User> attendees;
// Immutable meeting aggregate once scheduled
public Meeting(String id, User organizer, Room room, TimeSlot slot, List<User> attendees) {
this.id = id;
this.organizer = organizer;
this.room = room;
this.slot = slot;
this.attendees = List.copyOf(attendees);
}
// Unique meeting identifier for cancel/reschedule
public String getId() { return id; }
// User who created the booking
public User getOrganizer() { return organizer; }
// Physical or virtual room reserved for this meeting
public Room getRoom() { return room; }
// Start/end window of the meeting
public TimeSlot getSlot() { return slot; }
// All participants including organizer if listed
public List<User> getAttendees() { return attendees; }
}public class User {
private final String id;
private final String name;
private final List<Meeting> calendar = new ArrayList<>();
// Identity for conflict checks and ownership
public User(String id, String name) {
this.id = id;
this.name = name;
}
// Stable user id used as map key
public String getId() { return id; }
// Display name for logs and UI
public String getName() { return name; }
// Read-only view of this user's scheduled meetings
public List<Meeting> getCalendar() { return Collections.unmodifiableList(calendar); }
// Adds meeting to personal calendar after successful book
void addMeeting(Meeting meeting) {
calendar.add(meeting);
}
// Removes meeting on cancel; returns false if not found
boolean removeMeeting(String meetingId) {
return calendar.removeIf(m -> m.getId().equals(meetingId));
}
// True if any existing meeting overlaps the requested slot
boolean hasConflict(TimeSlot slot) {
return calendar.stream().anyMatch(m -> m.getSlot().overlaps(slot));
}
}
public class Room {
private final String id;
private final int capacity;
private final List<Meeting> bookings = new ArrayList<>();
// Room identity and max attendees allowed
public Room(String id, int capacity) {
this.id = id;
this.capacity = capacity;
}
// Room id for booking requests
public String getId() { return id; }
// Maximum attendees this room supports
public int getCapacity() { return capacity; }
// All meetings currently booked in this room
public List<Meeting> getBookings() { return Collections.unmodifiableList(bookings); }
// Registers a confirmed meeting on the room calendar
void addBooking(Meeting meeting) {
bookings.add(meeting);
}
// Drops a cancelled meeting from room schedule
void removeBooking(String meetingId) {
bookings.removeIf(m -> m.getId().equals(meetingId));
}
// True if room is already occupied during slot
boolean isAvailable(TimeSlot slot) {
return bookings.stream().noneMatch(m -> m.getSlot().overlaps(slot));
}
}Full solution — SchedulerService
public class SchedulerService {
private final Map<String, User> users = new HashMap<>();
private final Map<String, Room> rooms = new HashMap<>();
private final Map<String, Meeting> meetings = new HashMap<>();
// Registers a user for scheduling lookups
public void registerUser(User user) {
users.put(user.getId(), user);
}
// Registers a bookable room
public void registerRoom(Room room) {
rooms.put(room.getId(), room);
}
// Books room and updates all attendee calendars; throws on conflict
public synchronized Meeting schedule(
User organizer, Room room, TimeSlot slot, List<User> attendees) {
if (attendees.size() > room.getCapacity())
throw new IllegalStateException("Attendee count exceeds room capacity");
if (!room.isAvailable(slot))
throw new IllegalStateException("Room not available");
for (User attendee : attendees) {
User registered = users.get(attendee.getId());
if (registered == null)
throw new IllegalArgumentException("Unknown user: " + attendee.getId());
if (registered.hasConflict(slot))
throw new IllegalStateException("User busy: " + registered.getName());
}
String meetingId = UUID.randomUUID().toString();
Meeting meeting = new Meeting(meetingId, organizer, room, slot, attendees);
room.addBooking(meeting);
for (User attendee : attendees) {
users.get(attendee.getId()).addMeeting(meeting);
}
meetings.put(meetingId, meeting);
return meeting;
}
// Removes meeting from room and every attendee calendar
public synchronized void cancel(String meetingId) {
Meeting meeting = meetings.remove(meetingId);
if (meeting == null) return;
meeting.getRoom().removeBooking(meetingId);
for (User attendee : meeting.getAttendees()) {
users.get(attendee.getId()).removeMeeting(meetingId);
}
}
// Returns all meetings for a user sorted by start time
public List<Meeting> getMeetingsForUser(String userId) {
User user = users.get(userId);
if (user == null) return List.of();
return user.getCalendar().stream()
.sorted(Comparator.comparing(m -> m.getSlot().start()))
.toList();
}
}public class MeetingSchedulerDemo {
// Books two slots; second overlapping booking should fail
public static void main(String[] args) {
SchedulerService svc = new SchedulerService();
User alice = new User("u1", "Alice");
User bob = new User("u2", "Bob");
Room roomA = new Room("r1", 10);
svc.registerUser(alice);
svc.registerUser(bob);
svc.registerRoom(roomA);
TimeSlot morning = new TimeSlot(
LocalDateTime.of(2026, 5, 26, 9, 0),
LocalDateTime.of(2026, 5, 26, 10, 0));
Meeting m1 = svc.schedule(alice, roomA, morning, List.of(alice, bob));
System.out.println("Booked: " + m1.getId());
TimeSlot overlap = new TimeSlot(
LocalDateTime.of(2026, 5, 26, 9, 30),
LocalDateTime.of(2026, 5, 26, 10, 30));
try {
svc.schedule(bob, roomA, overlap, List.of(bob));
} catch (IllegalStateException e) {
System.out.println("Conflict: " + e.getMessage());
}
}
}While you code — questions interviewers ask
Extensions & follow-ups
- Reschedule — cancel + schedule in one transaction.
- Working hours / buffer between meetings.
- Priority booking for executives (override with audit log).
- External calendar sync (Google/Outlook) — HLD with webhooks.
Common mistakes (fail signals)
- Wrong overlap formula (
start1 <= start2 && end1 >= end2misses partial overlaps). - Checking room but forgetting attendee calendars.
- Not validating capacity or unknown user ids.
- Mutable TimeSlot shared and modified after book.