Skip to main content

Meeting Scheduler

Booking a room is easy. Booking a room and making sure the six people invited to it don't already have something else at that time is the actual problem - a scheduler that only checks the room is only doing half its job.

Requirements

Functional

  • An Attendee has a Calendar of Meetings.
  • Scheduling a meeting takes a room, a time range, and a list of attendees; it only succeeds if the room is free for that range and every attendee's calendar is free for that range.
  • If any conflict exists, scheduling fails and reports which resource (room or attendee) caused it, rather than silently double-booking.
  • A meeting can be cancelled, freeing the room and every attendee's calendar for that slot.

Non-functional

  • Conflict checking must be a single well-tested overlap rule reused for both rooms and attendees, not two copies of similar-but-subtly-different interval logic.
  • Checking a person's or room's free/busy status for a given range should be answerable without recomputing every meeting they've ever had from scratch each time (called out as a future index, kept simple here).

Design

Calendar is the one class that knows how to answer "is this range free" - both Room and Attendee hold one, and MeetingScheduler never inspects a meeting list directly. Booking a meeting is a two-phase check: gather every calendar involved (the room's plus each attendee's), confirm all of them are free, and only then commit the meeting everywhere - so a failed check never leaves the room booked while an attendee's calendar isn't, or vice versa.

OrganizerMeetingSchedulerRoomAttendeeschedule(room, attendees, range)1calendar.isFree(range)2calendar.isFree(range)3calendar.add(meeting)4calendar.add(meeting)5
  1. 1One call names every resource the meeting needs.
  2. 2The room's calendar is checked first.
  3. 3Every attendee's calendar is checked the same way, before anything commits.
  4. 4Only once every check passed does the meeting get written to the room's calendar.
  5. 5And to each attendee's calendar - all-or-nothing, never partial.

Meeting itself is inert data - a time range, a room, and a list of attendees - with no scheduling logic of its own. That logic belongs entirely to Calendar and MeetingScheduler, so a Meeting can be handed around, serialized, or compared without dragging any behavior along with it.

Class diagram

MeetingScheduler+ schedule(room, attendees, range): Meeting+ cancel(meeting)Calendar- meetings: List<Meeting>+ isFree(range): bool+ add(meeting)+ remove(meeting)Room- id: string- capacity: int- calendar: CalendarAttendee- id: string- name: string- calendar: CalendarMeeting- room: Room- attendees: List<Attendee>- from: datetime- to: datetime+ overlaps(range): bool
usescreates
MeetingScheduler checks the Room's Calendar and every Attendee's Calendar before committing a Meeting to any of them.

Code

import java.time.LocalDateTime;
import java.util.*;
 
class TimeRange {
final LocalDateTime from;
final LocalDateTime to;
 
TimeRange(LocalDateTime from, LocalDateTime to) {
this.from = from;
this.to = to;
}
 
boolean overlaps(TimeRange other) {
return from.isBefore(other.to) && other.from.isBefore(to);
}
}
 
class Meeting {
final Room room;
final List<Attendee> attendees;
final TimeRange range;
 
Meeting(Room room, List<Attendee> attendees, TimeRange range) {
this.room = room;
this.attendees = attendees;
this.range = range;
}
}
 
class Calendar {
private final List<Meeting> meetings = new ArrayList<>();
 
boolean isFree(TimeRange range) {
return meetings.stream().noneMatch(m -> m.range.overlaps(range));
}
 
void add(Meeting meeting) {
meetings.add(meeting);
}
 
void remove(Meeting meeting) {
meetings.remove(meeting);
}
}
 
class Room {
final String id;
final int capacity;
final Calendar calendar = new Calendar();
 
Room(String id, int capacity) {
this.id = id;
this.capacity = capacity;
}
}
 
class Attendee {
final String id;
final String name;
final Calendar calendar = new Calendar();
 
Attendee(String id, String name) {
this.id = id;
this.name = name;
}
}
 
class SchedulingConflictException extends RuntimeException {
SchedulingConflictException(String resource) {
super("Conflict on: " + resource);
}
}
 
class MeetingScheduler {
Meeting schedule(Room room, List<Attendee> attendees, TimeRange range) {
if (!room.calendar.isFree(range)) {
throw new SchedulingConflictException("room " + room.id);
}
for (Attendee a : attendees) {
if (!a.calendar.isFree(range)) {
throw new SchedulingConflictException("attendee " + a.id);
}
}
Meeting meeting = new Meeting(room, attendees, range);
room.calendar.add(meeting);
attendees.forEach(a -> a.calendar.add(meeting));
return meeting;
}
 
void cancel(Meeting meeting) {
meeting.room.calendar.remove(meeting);
meeting.attendees.forEach(a -> a.calendar.remove(meeting));
}
}

Design decisions

  • Calendar is shared by Room and Attendee rather than each having its own bespoke booking list. A room and a person need the exact same question answered - "is this range free, and if not, what conflicts" - so giving them the same class means the overlap rule is written and tested once, and a bug fix there fixes it for both.
  • Scheduling is check-everything-then-commit-everything, not check-and-commit per-resource. Committing to the room first and only then discovering attendee three is busy would leave the room reserved for a meeting that never actually happens. Checking every calendar before touching any of them is what keeps a failed booking from leaving partial state behind.
  • Conflict reporting names the specific resource that failed. Returning a bare "conflict" boolean forces the caller to re-check everything themselves to find out what to reschedule around; returning which room or attendee blocked it is what makes the failure actionable instead of just a rejection.
  • What's missing for a real system: Calendar's free-check here is a linear scan of that person's meetings, which is fine for a handful of meetings a day but should be an interval tree once someone has years of history, and recurring meetings (daily standup, weekly sync) aren't modeled - they'd need their own expansion step before hitting this same conflict check.

Common follow-ups

  • Why does schedule check every calendar before committing to any of them, instead of booking the room first and then each attendee in turn? Committing to the room and only then discovering attendee three is busy would leave the room reserved for a meeting that never actually happens - a partially-applied booking with no clean way to describe its state. Checking everything, then committing everything, is what keeps a failed attempt from leaving debris behind in any calendar.
  • Two organizers try to book the same room for overlapping times at nearly the same moment - what stops both from succeeding? As written, nothing does: isFree and add are two separate steps on Calendar, so both requests can see the room as free before either commits. The fix is the same shape as seat locking in movie-booking.mdx - the check-and-add on a given Calendar needs to happen as one atomic operation, not two.
  • How would you support "optional" attendees whose conflicts don't block scheduling? Add an isOptional flag to how an attendee is passed into schedule (or split the parameter into requiredAttendees and optionalAttendees), and only run the calendar.isFree check - and only call calendar.add - for required ones. MeetingScheduler would still commit the meeting to an optional attendee's calendar for visibility, it just wouldn't let their conflict block the booking.
  • How would recurring meetings (a daily standup) fit without rewriting the conflict check? Expand the recurrence into a list of concrete TimeRanges at scheduling time (today at 9am, tomorrow at 9am, ...) and run the existing all-or-nothing schedule against every occurrence in that list - if any single occurrence conflicts, the whole recurring series fails to schedule. Calendar.isFree never needs to know what "recurring" means; it only ever sees concrete ranges.

Check yourself

Question 1 of 3

Why is `Calendar` a single shared class used by both `Room` and `Attendee`, rather than each getting its own bespoke booking list?