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.
0%0 of 122 pages studied