Problem 06 · Complete walkthrough•Intermediate•45 min

Meeting Room Scheduler

Prefer to learn one decision at a time?

Try questions, reveal the reasoning, trace the code, and finish with the simulator.

Start Focus Mode

Design a meeting-room scheduler that finds a room with enough space and equipment, prevents time conflicts, and makes the room reusable after cancellation.

A team of four requests a meeting with a projector while an occupied room and an unsuitable room are rejected and the smallest suitable free room is highlighted
A room is useful only when three answers are yes at the same time: it fits the people, has the required equipment, and is free for the complete time slot.

1. Requirements (~5 minutes)

The interview prompt is usually short:

“Design a meeting-room scheduler.”

We all understand meetings and rooms, but that familiarity can hide unanswered questions. Does the caller choose a room? Does a meeting ending at 10:00 conflict with one starting at 10:00? Are attendee calendars included? Can anybody cancel another person's meeting?

Do not begin with a Room class. First turn the vague prompt into actions whose results both you and the interviewer can predict.

Use these four question groups every time

1
Actions

What must a user be able to do?

2
Rules

When does an action succeed, fail, or change state?

3
Errors

Which invalid actions must we reject?

4
Boundaries

What should we deliberately not build?

Ask these questions

AskWhich actions must the first version support?

Interviewer answer

An organizer submits one meeting request. The system chooses a room and returns a meeting ID. The same organizer may later cancel it.

AskWhat makes a room suitable before we check its calendar?

Interviewer answer

A request contains an attendee count and zero or more required equipment values. A room needs enough capacity and every required item.

AskWho chooses the room, and which suitable room wins?

Interviewer answer

The system chooses the smallest suitable available room. If capacities tie, choose the lower room ID. Keep the policy replaceable.

AskDoes a meeting from 09:00 to 10:00 block a meeting starting exactly at 10:00?

Interviewer answer

No. Treat every slot as [start, end): start is included and end is excluded.

AskWhat happens when no room satisfies every rule?

Interviewer answer

Return NO_ROOM_AVAILABLE. Do not create a meeting ID or reserve any room.

AskWho may cancel a meeting, and what does cancellation change?

Interviewer answer

Only the organizer may cancel. An unknown ID or wrong organizer is rejected without changing the schedule.

AskWhich time-zone, attendee, recurrence, storage, and concurrency rules are in scope?

Interviewer answer

Use one office time zone and pre-created rooms. Calls reach one in-memory process sequentially. Leave recurring meetings, attendee calendars, persistence, and simultaneous callers for extensions.

Confirm the specification

Confirmed specification

8 requirements · 8 boundaries

The first version must

  1. 1.Rooms are pre-created with a unique ID, name, capacity, and equipment set.
  2. 2.A request contains a title, organizer ID, attendee count, required equipment, start time, and end time.
  3. 3.A valid time slot uses [start, end): start must be before end, and back-to-back meetings are allowed.
  4. 4.A room is suitable only when it has enough capacity and all required equipment.
  5. 5.A room is available only when the requested slot overlaps none of its existing meetings.
  6. 6.Choose the available suitable room with the smallest capacity; break ties by room ID.
  7. 7.If no room qualifies, return NO_ROOM_AVAILABLE and change nothing.
  8. 8.Only the organizer may cancel a known meeting; successful cancellation frees its room and slot.
Not building in this version8 boundaries
  • Recurring meetings
  • Attendee calendars and invitations
  • Time-zone conversion
  • Room setup or cleanup buffers
  • Persistent storage
  • Concurrent or multi-server requests
  • Notifications and email
  • Changing an existing meeting in place

2. Entities and relationships (~3 minutes)

Scan the confirmed requirements for nouns: organizer, meeting request, time slot, meeting, room, equipment, calendar, selection rule, and scheduler. These are candidates, not automatic classes. Ask what each one must remember or decide.

Final model

Types we will create

Class
Owns changing state or coordinates rules.
Record
A Java record for validated, immutable data with value equality.
Interface
Defines behavior that another implementation can replace.
TimeSlotRecord

Validates boundaries and owns overlap reasoning.

MeetingRequestRecord

Carries one organizer's scheduling intent.

MeetingRecord

Records one accepted room reservation.

RoomClass

Owns suitability and its ordered schedule.

RoomSelectionStrategyInterface

Keeps deterministic room choice replaceable.

MeetingSchedulerClass

Coordinates selection, IDs, indexing, and cancellation.

ScheduleResultRecord

Returns a named scheduling outcome.

Enums

Enums name a small, fixed set of valid choices or outcomes.

Equipment

Names the room capabilities a request may require.

ScheduleStatus

Explains the result of scheduling a meeting.

CancellationStatus

Explains the result of cancelling a meeting.

Fields

Fields are simple values stored inside another type; they do not need independent behavior in this version.

organizerId

Identifies who may cancel an accepted meeting.

attendeeCount

Defines the minimum room capacity required.

roomId

Identifies the room assigned to a meeting.

meetingId

Finds an accepted meeting for later operations.

Supporting implementation

Room TreeMap schedule

Keeps meetings ordered by start time for neighbour checks.

See one request move through the objects

Swipe to follow the flow →

An organizer sends a time request to MeetingScheduler, which asks RoomSelectionStrategy to check Room capacity, equipment, schedule, and TimeSlot before creating a Meeting and returning a result
The strategy compares rooms, but each Room owns the final answer about its suitability and schedule.

Read the arrows from left to right. The Organizer submits a request to MeetingScheduler. The scheduler asks RoomSelectionStrategy to compare rooms. The strategy calls each Room, and Room uses its capacity, equipment, ordered meetings, and the requested TimeSlot. The chosen Room reserves the slot, which allows the scheduler to create a Meeting. If no Room qualifies, the same path returns a rejection and no Meeting is created.

The boundary matters: the strategy may choose a Room, but it cannot write into that Room's schedule. Room.reserve repeats the availability check and owns the state change.

3. Class design (10–15 minutes)

Move from the entities into methods using the same chain:

requirement → responsible object → state it remembers → method that changes or reads that state

Start at MeetingScheduler.schedule because that is the caller-visible action. Then follow its questions downward: which rooms qualify, and how does one Room know that its time is free?

One class at a time

Design the state owners

Start with the coordinator, then follow the request into the classes that protect changing state. Select a class to see only the fields and behavior needed for its responsibility.

Class 1 of 2

MeetingScheduler

Coordinates deterministic room selection, accepted meetings, IDs, and cancellation.

Requirement behind this class

Schedule the smallest qualified room, cancel only for the owner, and leave accepted state unchanged after rejection.

State it remembers

Private information required to perform this class's job.

List<Room> rooms

Provides the immutable selection candidates.

Map<String, Room> roomsById

Finds the assigned room during cancellation and reads.

Map<String, Meeting> meetingsById

Finds accepted meetings by ID.

RoomSelectionStrategy strategy

Delegates replaceable qualification and comparison.

int nextMeetingNumber

Allocates IDs only after a room can be reserved.

Behavior it owns

Public use cases and internal rules kept beside that state.

schedule(request)Public API

Selects a Room, constructs a Meeting, reserves it, then indexes success.

cancel(meetingId, organizerId)Public API

Checks existence and ownership before removing from Room and index.

meetingsForRoom(roomId)Public API

Returns a safe ordered meeting view.

validate model / advance IDInternal

Keep duplicate rooms and rejected ID consumption out of accepted state.

Invariant it protects
The meeting index and each Room schedule contain the same accepted reservations.
Works with
Room, RoomSelectionStrategy, MeetingRequest, Meeting, and ScheduleResult.
Does not own
Interval overlap, room suitability, or direct TreeMap mutation.
Principle demonstrated
Validation before mutation keeps IDs, indexes, and room schedules consistent.

Supporting types

These types carry stable data or define replaceable behavior. They support the state owners without becoming another place to store live workflow state.

TimeSlot, MeetingRequest, Meeting, ScheduleResultRecord

Carry validated interval, intent, accepted reservation, and outcome data.

TimeSlot owns half-open overlap reasoning while the other records remain stable boundary values.

RoomSelectionStrategyInterface

Defines how qualified rooms are compared.

It reads Rooms and returns a choice; it cannot reserve or mutate a schedule.

SmallestSuitableRoomStrategyImplementation

Filters suitability and chooses capacity then room ID.

The comparator makes equal candidates deterministic without moving workflow into the Strategy.

The explorer shows that Room—not MeetingScheduler—owns interval and schedule state. The next sections explain the half-open interval and TreeMap neighbour rules that make Room's invariant efficient.

Scheduling concept: half-open intervals

Write a time slot as [start, end). The square bracket means the start belongs to the meeting. The round bracket means the end does not.

For a meeting [09:00, 10:00), the occupied times include 09:00 and 09:59, but not 10:00. Therefore [10:00, 10:30) can begin immediately.

Two slots overlap only when both statements are true:

first.start < second.end
second.start < first.end

Why use two comparisons? Consider [09:00, 10:00) and [10:00, 10:30):

  • 09:00 < 10:30 is true.
  • 10:00 < 10:00 is false.
  • Both are not true, so the slots do not overlap.

Using <= would wrongly reject every back-to-back meeting. That one character is a common interview bug.

  1. 1Focus contains [09:00, 10:00).
  2. 2A request asks for [10:00, 10:30).
  3. 3Existing start 09:00 is before requested end 10:30.
  4. 4Requested start 10:00 is not before existing end 10:00.
  5. 5The slots only touch, so Focus is available.

Why an ordered schedule needs only two neighbour checks

Room stores accepted meetings in a TreeMap keyed by start time. A TreeMap keeps keys sorted and can quickly find the closest entry before or after a requested start.

Suppose the schedule is:

[08:30, 09:00)   [09:00, 10:00)   [10:30, 11:00)

For a new [10:00, 10:30) slot:

  1. floorEntry(10:00) returns [09:00, 10:00), the closest meeting starting at or before 10:00.
  2. It touches but does not overlap.
  3. ceilingEntry(10:00) returns [10:30, 11:00), the closest meeting starting at or after 10:00.
  4. It also touches but does not overlap.
  5. No more distant meeting can overlap without crossing one of those neighbours.

This last step depends on an invariant: a promise the object always keeps. Room never stores overlapping meetings. Because existing entries do not overlap, checking the predecessor and successor is sufficient.

If one room has M meetings, each neighbour lookup costs O(log M). Checking R rooms costs O(R log M) in the simplified worst case, and inserting the accepted meeting costs another O(log M). Total space is O(R + N) for rooms and all N accepted meetings.

Understand deterministic room selection

The strategy filters in this order:

  1. Enough capacity.
  2. Every required equipment item.
  3. No overlapping meeting.
  4. Among survivors, lower capacity wins.
  5. If capacity ties, lower room ID wins.

For four people needing a projector, a free 4-seat room beats a free 10-seat room. This preserves the large room for a later request that may actually need it. The ID comparison is not a business optimization; it makes equal cases predictable.

RoomSelectionStrategy owns this comparison and nothing else. It may call Room.canHost, but it cannot reserve the winner, allocate an ID, or update scheduler indexes. That read-only boundary lets a future policy change without moving the Room invariant or cancellation workflow.

Decide whether patterns help

Use: Strategy for room selection

The requirement explicitly says selection may change. RoomSelectionStrategy.select(rooms, request) is the boundary. SmallestSuitableRoomStrategy implements today's capacity-and-ID rule. A future nearest-floor strategy can replace it without changing interval checks, meeting creation, or cancellation.

Do not force: Observer for invitations

The first version sends no invitations or notifications. There are no listeners to notify. Add Observer-like event handling only when a confirmed requirement introduces several reactions to a successful schedule or cancellation.

Do not force: Singleton MeetingScheduler

A global instance does not prevent conflicts across servers and makes tests share mutable state. Normal construction keeps rooms and strategy explicit.

Do not force: Factory for Meeting

Meeting construction is one visible record constructor after room selection. A factory would hide simple values without solving a creation rule. Add one only if creation later needs several meeting types or external configuration.

Prove the scheduling rule interactively

The default request starts exactly when Focus becomes free. Schedule it to prove the half-open boundary. Then load the overlap edge to see every room's rejection reason, or the capacity edge to see why Atlas is the only candidate.

Try the interval rule

Meeting room scheduler

08:00–12:00
08:0008:3009:0009:3010:0010:3011:0011:30
Focus

4 · projector, whiteboard

suitable and available
Daily sync
Cedar

6 · whiteboard

does not have projector
Planning
Atlas

10 · projector, video, whiteboard

overlaps 09:30–10:30
Customer call

Selection order: suitable capacity and equipment → no overlap → smallest capacity → room ID.

New meeting request

Half-open slot: [10:00, 10:30). A meeting ending at 10:00 does not conflict.

What the scheduler decided

  1. Ready: try 10:00–10:30. Focus becomes free exactly at 10:00.

Connect the classes and review the final blueprint

MeetingScheduler has Rooms and one RoomSelectionStrategy. Each Room owns its ordered Meetings, and every Meeting contains one TimeSlot. These composition relationships match the responsibilities; none of the classes is a specialized form of another.

  • Immutability: requests, time slots, and accepted meetings cannot change behind the schedule.
  • Encapsulation: Room never exposes its mutable TreeMap and validates every insertion.
  • Single responsibility: TimeSlot owns overlap, Room owns one schedule, Strategy selects, and Scheduler coordinates.
  • Open/closed design: a new room policy implements the Strategy boundary without changing interval correctness.
  • Validation before mutation: failed scheduling and cancellation leave schedules, indexes, and the next accepted ID unchanged.
Sketch UML of meeting records

How to read it: TimeSlot, request, meeting, and result records preserve requested and accepted facts.

Design principle: Immutability prevents accepted reservations changing later.

4. Java implementation (~10 minutes)

What the code does

This Java 17 program models room capacity and equipment, half-open time slots, ordered per-room schedules, deterministic smallest-room selection, accepted meetings, and owner-only cancellation.

Why it is structured this way

TimeSlot owns interval math. Room owns schedule correctness. Strategy compares qualified rooms. MeetingScheduler coordinates IDs and cancellation without reaching into a Room's private schedule.

Follow one request through the code

  1. 1MeetingScheduler receives one immutable MeetingRequest.
  2. 2SmallestSuitableRoomStrategy asks every Room whether it can host the request.
  3. 3Room checks capacity, equipment, and the two neighbouring schedule entries.
  4. 4The strategy selects the smallest capacity, then the lowest room ID.
  5. 5Room.reserve repeats validation and inserts the Meeting into its TreeMap.
  6. 6MeetingScheduler records the accepted Meeting by ID and returns it.

Code concepts to understand

Half-open TimeSlotOne value object keeps boundary behavior identical everywhere.

TimeSlot.overlaps uses two strict isBefore comparisons. Strict comparisons make equal end and start times non-overlapping. The constructor rejects an end that is equal to or before the start, so every created TimeSlot is meaningful.

TreeMap predecessor and successorAn ordered, non-overlapping schedule avoids scanning every meeting.

floorEntry(request.start) finds the closest existing start at or before the request. ceilingEntry(request.start) finds the closest at or after it. Room checks both with the same TimeSlot.overlaps method. This is correct because reserve never permits existing meetings to overlap.

Immutable request and result recordsRequested values and accepted facts cannot be edited behind the scheduler's back.

MeetingRequest, TimeSlot, and Meeting are records. Their constructors copy equipment sets, so a caller cannot modify the original Set after construction and silently change the request or meeting.

Validation before state changeA rejection leaves every room exactly as it was.

Strategy checks all qualifications before a Meeting exists. Room.reserve checks again before TreeMap.put. MeetingScheduler adds the global index only after reserve succeeds. Cancellation checks existence and ownership before removing from either place.

Strategy call flowThe pattern isolates only the rule that actually varies.

MeetingScheduler calls strategy.select(rooms, request). The strategy does not inspect private maps; it calls room.canHost(request). The returned Room still performs reserve. Selection and ownership of schedule state remain separate.

Start with the fixed values and interval object

Read TimeSlot first. Every later availability decision depends on its boundary rule. Then read the two immutable records that distinguish a requested meeting from an accepted one.

Equipment.java
Names the equipment values supported by the first version.
public enum Equipment {
  PROJECTOR,
  WHITEBOARD,
  VIDEO
}
TimeSlot.java
Validates and compares half-open [start, end) intervals.
import java.time.LocalDateTime;

public record TimeSlot(LocalDateTime start, LocalDateTime end) {
  public TimeSlot {
      if (start == null || end == null) {
          throw new IllegalArgumentException("Start and end are required");
      }
      if (!start.isBefore(end)) {
          throw new IllegalArgumentException("Start must be before end");
      }
  }

  public boolean overlaps(TimeSlot other) {
      if (other == null) {
          throw new IllegalArgumentException("Other slot is required");
      }
      return start.isBefore(other.end)
              && other.start.isBefore(end);
  }
}
MeetingRequest.java
Carries and validates one organizer's requested meeting.
import java.util.Set;

public record MeetingRequest(
      String title,
      String organizerId,
      TimeSlot slot,
      int attendeeCount,
      Set<Equipment> requiredEquipment
) {
  public MeetingRequest {
      if (title == null || title.isBlank()) {
          throw new IllegalArgumentException("Title is required");
      }
      if (organizerId == null || organizerId.isBlank()) {
          throw new IllegalArgumentException("Organizer is required");
      }
      if (slot == null || attendeeCount <= 0 || requiredEquipment == null) {
          throw new IllegalArgumentException("Valid slot, attendees, and equipment are required");
      }
      requiredEquipment = Set.copyOf(requiredEquipment);
  }
}
Meeting.java
Stores the immutable facts of one accepted room reservation.
import java.util.Set;

public record Meeting(
      String id,
      String title,
      String organizerId,
      String roomId,
      TimeSlot slot,
      int attendeeCount,
      Set<Equipment> requiredEquipment
) {
  public Meeting {
      if (id == null || id.isBlank()
              || title == null || title.isBlank()
              || organizerId == null || organizerId.isBlank()
              || roomId == null || roomId.isBlank()) {
          throw new IllegalArgumentException("Meeting identity fields are required");
      }
      if (slot == null || attendeeCount <= 0 || requiredEquipment == null) {
          throw new IllegalArgumentException("Valid slot, attendees, and equipment are required");
      }
      requiredEquipment = Set.copyOf(requiredEquipment);
  }
}

Add clear result types

Expected scheduling and cancellation failures are normal outcomes, not programming exceptions. These enums let a caller distinguish “no room” from “wrong organizer” without parsing text.

ScheduleStatus.java
Names the accepted and rejected schedule outcomes.
public enum ScheduleStatus {
  SCHEDULED,
  NO_ROOM_AVAILABLE
}
CancellationStatus.java
Names every expected cancellation outcome.
public enum CancellationStatus {
  CANCELED,
  MEETING_NOT_FOUND,
  NOT_ORGANIZER
}
ScheduleResult.java
Carries a status and the accepted Meeting when scheduling succeeds.
import java.util.Optional;

public record ScheduleResult(
      ScheduleStatus status,
      Optional<Meeting> meeting
) {
  public ScheduleResult {
      if (status == null || meeting == null) {
          throw new IllegalArgumentException("Result fields are required");
      }
  }

  public static ScheduleResult accepted(Meeting meeting) {
      return new ScheduleResult(
              ScheduleStatus.SCHEDULED,
              Optional.of(meeting)
      );
  }

  public static ScheduleResult rejected() {
      return new ScheduleResult(
              ScheduleStatus.NO_ROOM_AVAILABLE,
              Optional.empty()
      );
  }
}

Let Room protect the ordered schedule

Read isAvailable before reserve. The first method explains the neighbour algorithm; the second shows how the state change remains behind the same validation boundary.

Room.java
Owns suitability facts, ordered meetings, overlap checks, reservation, and removal.
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public final class Room {
  private final String id;
  private final String name;
  private final int capacity;
  private final Set<Equipment> equipment;
  private final TreeMap<LocalDateTime, Meeting> schedule = new TreeMap<>();

  public Room(String id, String name, int capacity, Set<Equipment> equipment) {
      if (id == null || id.isBlank() || name == null || name.isBlank()) {
          throw new IllegalArgumentException("Room id and name are required");
      }
      if (capacity <= 0 || equipment == null) {
          throw new IllegalArgumentException("Valid capacity and equipment are required");
      }
      this.id = id;
      this.name = name;
      this.capacity = capacity;
      this.equipment = Set.copyOf(equipment);
  }

  public String id() {
      return id;
  }

  public String name() {
      return name;
  }

  public int capacity() {
      return capacity;
  }

  public Set<Equipment> equipment() {
      return equipment;
  }

  public boolean canHost(MeetingRequest request) {
      return capacity >= request.attendeeCount()
              && equipment.containsAll(request.requiredEquipment())
              && isAvailable(request.slot());
  }

  public boolean isAvailable(TimeSlot requested) {
      Map.Entry<LocalDateTime, Meeting> previous =
              schedule.floorEntry(requested.start());
      if (previous != null && previous.getValue().slot().overlaps(requested)) {
          return false;
      }

      Map.Entry<LocalDateTime, Meeting> next =
              schedule.ceilingEntry(requested.start());
      return next == null || !next.getValue().slot().overlaps(requested);
  }

  public boolean reserve(Meeting meeting) {
      if (!id.equals(meeting.roomId())
              || capacity < meeting.attendeeCount()
              || !equipment.containsAll(meeting.requiredEquipment())
              || !isAvailable(meeting.slot())) {
          return false;
      }
      schedule.put(meeting.slot().start(), meeting);
      return true;
  }

  public boolean remove(Meeting meeting) {
      if (meeting == null || !id.equals(meeting.roomId())) {
          return false;
      }
      return schedule.remove(meeting.slot().start(), meeting);
  }

  public List<Meeting> meetings() {
      return List.copyOf(schedule.values());
  }
}

Isolate the selection rule

The interface is deliberately small. Its implementation filters by Room.canHost and uses min, so it does not need to sort every qualified room.

RoomSelectionStrategy.java
Defines the one room-choice behavior allowed to vary.
import java.util.List;
import java.util.Optional;

public interface RoomSelectionStrategy {
  Optional<Room> select(List<Room> rooms, MeetingRequest request);
}
SmallestSuitableRoomStrategy.java
Chooses the smallest qualified room, then the lowest room ID.
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public final class SmallestSuitableRoomStrategy
      implements RoomSelectionStrategy {

  @Override
  public Optional<Room> select(
          List<Room> rooms,
          MeetingRequest request
  ) {
      return rooms.stream()
              .filter(room -> room.canHost(request))
              .min(Comparator.comparingInt(Room::capacity)
                      .thenComparing(Room::id));
  }
}

Finish with the coordinator and demo

MeetingScheduler.schedule orders the complete workflow. cancel deliberately checks everything before the first removal, so an unknown ID or wrong organizer changes neither the Room nor the global index.

MeetingScheduler.java
Coordinates room selection, accepted IDs, meeting lookup, and owner-only cancellation.
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public final class MeetingScheduler {
  private final List<Room> rooms;
  private final Map<String, Room> roomsById;
  private final Map<String, Meeting> meetingsById = new LinkedHashMap<>();
  private final RoomSelectionStrategy strategy;
  private int nextMeetingNumber = 1;

  public MeetingScheduler(
          List<Room> rooms,
          RoomSelectionStrategy strategy
  ) {
      if (rooms == null || rooms.isEmpty() || strategy == null) {
          throw new IllegalArgumentException("Rooms and strategy are required");
      }
      this.rooms = List.copyOf(rooms);
      this.strategy = strategy;
      this.roomsById = new LinkedHashMap<>();
      for (Room room : rooms) {
          if (roomsById.putIfAbsent(room.id(), room) != null) {
              throw new IllegalArgumentException("Duplicate room: " + room.id());
          }
      }
  }

  public ScheduleResult schedule(MeetingRequest request) {
      if (request == null) {
          throw new IllegalArgumentException("Request is required");
      }

      Optional<Room> selected = strategy.select(rooms, request);
      if (selected.isEmpty()) {
          return ScheduleResult.rejected();
      }

      Room room = selected.get();
      String meetingId = "M" + nextMeetingNumber;
      Meeting meeting = new Meeting(
              meetingId,
              request.title(),
              request.organizerId(),
              room.id(),
              request.slot(),
              request.attendeeCount(),
              request.requiredEquipment()
      );

      if (!room.reserve(meeting)) {
          return ScheduleResult.rejected();
      }
      nextMeetingNumber++;
      meetingsById.put(meeting.id(), meeting);
      return ScheduleResult.accepted(meeting);
  }

  public CancellationStatus cancel(
          String meetingId,
          String organizerId
  ) {
      Meeting meeting = meetingsById.get(meetingId);
      if (meeting == null) {
          return CancellationStatus.MEETING_NOT_FOUND;
      }
      if (!meeting.organizerId().equals(organizerId)) {
          return CancellationStatus.NOT_ORGANIZER;
      }

      Room room = roomsById.get(meeting.roomId());
      if (!room.remove(meeting)) {
          throw new IllegalStateException("Meeting index and room disagree");
      }
      meetingsById.remove(meetingId);
      return CancellationStatus.CANCELED;
  }

  public List<Meeting> meetingsForRoom(String roomId) {
      Room room = roomsById.get(roomId);
      return room == null ? List.of() : room.meetings();
  }
}
MeetingRoomSchedulerDemo.java
Exercises boundaries, fallback selection, rejection, cancellation, and reuse.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.Set;

public final class MeetingRoomSchedulerDemo {
  public static void main(String[] args) {
      LocalDate day = LocalDate.of(2026, 8, 14);
      MeetingScheduler scheduler = sampleScheduler();

      TimeSlot firstSlot = slot(day, 9, 0, 10, 0);
      TimeSlot touchingSlot = slot(day, 10, 0, 10, 30);
      System.out.println("back-to-back overlap: "
              + firstSlot.overlaps(touchingSlot));

      ScheduleResult first = scheduler.schedule(request(
              "Daily sync", "organizer-a", firstSlot, 4
      ));
      System.out.println("first room: "
              + first.meeting().orElseThrow().roomId());

      ScheduleResult touching = scheduler.schedule(request(
              "Design review", "organizer-b", touchingSlot, 4
      ));
      System.out.println("back-to-back room: "
              + touching.meeting().orElseThrow().roomId());

      ScheduleResult fallback = scheduler.schedule(request(
              "Interview", "organizer-c", slot(day, 9, 30, 10, 0), 4
      ));
      System.out.println("conflict fallback room: "
              + fallback.meeting().orElseThrow().roomId());

      ScheduleResult rejected = scheduler.schedule(request(
              "Urgent review", "organizer-d", slot(day, 9, 45, 10, 0), 4
      ));
      System.out.println("fully occupied: " + rejected.status());

      String firstId = first.meeting().orElseThrow().id();
      System.out.println("wrong cancellation: "
              + scheduler.cancel(firstId, "organizer-b"));
      System.out.println("owner cancellation: "
              + scheduler.cancel(firstId, "organizer-a"));

      ScheduleResult reopened = scheduler.schedule(request(
              "Replacement", "organizer-e", slot(day, 9, 0, 9, 30), 4
      ));
      System.out.println("reopened room: "
              + reopened.meeting().orElseThrow().roomId());
  }

  private static MeetingScheduler sampleScheduler() {
      Room focus = new Room(
              "R1", "Focus", 4,
              Set.of(Equipment.PROJECTOR, Equipment.WHITEBOARD)
      );
      Room cedar = new Room(
              "R2", "Cedar", 6,
              Set.of(Equipment.WHITEBOARD)
      );
      Room atlas = new Room(
              "R3", "Atlas", 10,
              Set.of(
                      Equipment.PROJECTOR,
                      Equipment.WHITEBOARD,
                      Equipment.VIDEO
              )
      );
      return new MeetingScheduler(
              List.of(focus, cedar, atlas),
              new SmallestSuitableRoomStrategy()
      );
  }

  private static MeetingRequest request(
          String title,
          String organizer,
          TimeSlot slot,
          int attendees
  ) {
      return new MeetingRequest(
              title,
              organizer,
              slot,
              attendees,
              Set.of(Equipment.PROJECTOR)
      );
  }

  private static TimeSlot slot(
          LocalDate day,
          int startHour,
          int startMinute,
          int endHour,
          int endMinute
  ) {
      return new TimeSlot(
              LocalDateTime.of(day, LocalTime.of(startHour, startMinute)),
              LocalDateTime.of(day, LocalTime.of(endHour, endMinute))
      );
  }
}

Trace the compiled behavior

  1. 1The first [09:00, 10:00) request needs four seats and a projector. Focus is the smallest qualified room, so M1 is inserted there.
  2. 2The [10:00, 10:30) request touches M1's end but does not overlap. Focus is selected again.
  3. 3A [09:30, 10:00) request conflicts in Focus. Cedar lacks a projector, so Atlas becomes the valid fallback.
  4. 4A [09:45, 10:00) request now conflicts in Focus and Atlas while Cedar is unsuitable. It returns NO_ROOM_AVAILABLE and creates no ID.
  5. 5A different organizer tries to cancel M1. The scheduler returns NOT_ORGANIZER and both indexes stay unchanged.
  6. 6The real organizer cancels M1. Room.remove frees [09:00, 10:00), and a replacement request chooses Focus again.

5. Extensions (~5 minutes)

Each follow-up begins by changing one requirement, then changing the smallest responsible boundary.

Add room setup and cleanup buffers

If every meeting needs ten minutes of preparation, the requested interval used for conflict checking expands before selection. TimeSlot can expose a buffered copy, while the Meeting still stores the visible start and end. Clarify whether back-to-back meetings are still allowed before changing the formula.

Add recurring meetings

A recurring request is not one Meeting. Add a recurrence rule that expands into a finite set of TimeSlots for a stated date range. Scheduling must validate all occurrences before reserving any, or roll back earlier inserts if a later date fails. Do not store “forever” recurrence without a boundary.

Add attendee calendars

The new requirement creates an Attendee calendar owner. Room availability and person availability become separate checks. MeetingScheduler coordinates both, but each calendar should still own its own overlap rule. Invitations and response states enter only if the interviewer asks for them.

Handle simultaneous requests

The current check-then-reserve flow assumes sequential calls. In one Java process, Room.reserve could protect its neighbour check and insert with a lock. Across application servers, use a shared database transaction or constraint. A Strategy result alone is never a distributed reservation guarantee.

Search a large organization efficiently

With thousands of rooms, first index fixed properties such as building, capacity, and equipment. The scheduler retrieves a smaller candidate set, then checks each candidate's ordered schedule. Keep the exact capacity and room-ID tie-breakers so the result remains deterministic.