Low Level Design
Design a Conference Room Booking System
Low-level design of a thread-safe office room booking system featuring Singleton, Facade, Strategy (pluggable room selection), Observer (Email/Calendar/Slack notifications), Builder for recurring bookings, and atomic rollback.
Problem Description#
Design a Conference Room Booking System for an office environment that lets employees book available rooms for meetings, handle recurring schedules, and prevent double-booking.
The challenge here isn't just the booking logic itself — it's building a system that stays correct under concurrent access, supports different room-selection strategies, and notifies multiple downstream consumers (email, calendar, Slack) without tight coupling. It's a real-world problem where thread safety, extensibility, and the single-responsibility principle all pull at each other simultaneously.
Clarify Requirements#
Functional
- Can administrators register rooms with a name, type (Small/Large), and available time slots?
- Can employees register themselves and view their own upcoming bookings?
- Should booking find the best available room automatically, or does the caller pick a room?
- Do we need support for recurring bookings (weekly, daily, biweekly, monthly)?
- Should a partial conflict on a recurring series abort the whole booking or allow partial success?
- What notifications should fire on booking creation and cancellation?
- Should booking strategies be swappable at runtime?
Non-functional
- Must the system be thread-safe for concurrent bookings?
- Should booking strategies be hot-swappable without restarting the system?
- Is a single global system instance required (no multi-tenant)?
Final Requirements#
- Two room types: SMALL (≤10 people) and LARGE (≤30 people); slots are hourly blocks from 9 AM – 7 PM
- Employee registration with duplicate check; bookings are per-employee
- Single bookings: auto-select the best available room via a pluggable strategy (Best Fit, First Available, Largest Available)
- Recurring bookings: DAILY, WEEKLY, BIWEEKLY, MONTHLY; atomic — either all occurrences book or none do (rollback on conflict)
- Observer notifications: Email, Calendar, Slack — fired on booking creation and cancellation; new observers can be added at runtime
- Builder pattern for constructing Recurrence with validation and defaults
- Thread-safe throughout: single-lock strategy per room to eliminate deadlock risk
Core Entities#
| Entity | Responsibility |
|---|---|
| RoomBookingSystem | Singleton + Facade — single entry point; initializes default observers; delegates to Orchestrator |
| RoomBookingOrchestrator | Core business logic — validates inputs, finds rooms via strategy, creates bookings, notifies observers |
| RoomInventory | ConcurrentHashMap-backed repository for rooms; returns safe snapshots |
| EmployeeInventory | ConcurrentHashMap-backed repository for employees |
| Room | Manages bookings per day via TreeMap<LocalTime, Booking>; O(log n) conflict detection; all mutating methods synchronized |
| Employee | Holds employee metadata and a CopyOnWriteArrayList of bookings |
| Booking | Immutable record of one booking: room, start/end time, day index, employee name, UUID |
| Recurrence | Immutable value object for recurring schedules; built via inner Builder with validation |
| RoomStrategy | Interface for single-booking room selection |
| RecurringRoomStrategy | Interface for recurring-booking room selection |
| BookingObserver | Interface for notification subscribers |
| RoomType | Enum: SMALL, LARGE |
| FrequencyType | Enum: DAILY, WEEKLY, BIWEEKLY, MONTHLY |
Patterns Used#
Singleton — RoomBookingSystem#
There must be exactly one booking system across the application. The Singleton is implemented with a synchronized getInstance() method and a private constructor that initializes repositories and default observers.
RoomBookingSystem.getInstance()
→ creates RoomBookingOrchestrator (once)
→ subscribes EmailObserver, CalendarObserver, SlackObserver
Facade — RoomBookingSystem#
The Singleton also acts as a Facade: it exposes simple methods (registerRoom, bookRoom, viewSchedule) and hides the complexity of the Orchestrator, Inventory, and Strategy layers underneath.
Strategy — RoomStrategy / RecurringRoomStrategy#
Room selection is extracted into swappable strategies rather than hardcoded:
- BestFitStrategy (default) — prefers smaller rooms, falls back to larger. Minimizes wasted capacity.
- FirstAvailableStrategy — returns the first room that fits, for speed.
- LargestAvailableStrategy — prefers large rooms; useful for premium allocation.
Each strategy implements the same interface so the Orchestrator never needs to change when a new algorithm is added.
Observer — BookingObserver#
Notifications (email, calendar sync, Slack) are decoupled from the booking flow. The Orchestrator holds a CopyOnWriteArrayList<BookingObserver> and calls notifyBookingCreated() / notifyBookingCancelled() after each successful operation. New notification channels are added with a single subscribe() call — zero changes to booking logic.
Builder — Recurrence.Builder#
Recurrence has multiple required fields and an optional frequency (defaulting to WEEKLY). Without a builder:
// Unclear — what does 3 mean? What does 1 mean?
new Recurrence(4, LocalTime.of(11,0), LocalTime.of(12,0), 3, "WEEKLY");
With the Builder:
new Recurrence.Builder(4, LocalTime.of(11,0), LocalTime.of(12,0), 3)
.withFrequency("WEEKLY")
.build(); // validates all fields before constructing
build() validates the time range, business-hours bounds, and day-of-week before creating the immutable Recurrence object.
Repository — RoomInventory / EmployeeInventory#
Data access is abstracted behind repository classes backed by ConcurrentHashMap. Getters return defensive copies so callers can iterate safely without holding locks.
Dependency Injection — RoomBookingOrchestrator#
The Orchestrator receives RoomInventory and EmployeeInventory through its constructor rather than creating them internally. This makes the Orchestrator independently testable and decoupled from specific implementations.
Code#
Core Service & Orchestrator#
package RoomBookingSystem.service;
import RoomBookingSystem.model.Recurrence;
import RoomBookingSystem.orchestrator.RoomBookingOrchestrator;
import RoomBookingSystem.repository.EmployeeInventory;
import RoomBookingSystem.repository.RoomInventory;
import RoomBookingSystem.observer.EmailObserver;
import RoomBookingSystem.observer.CalendarObserver;
import RoomBookingSystem.observer.SlackObserver;
import java.time.LocalTime;
import java.util.List;
public class RoomBookingSystem {
private static RoomBookingSystem systemInstance;
private final RoomBookingOrchestrator orchestrator;
private RoomBookingSystem() {
this.orchestrator = new RoomBookingOrchestrator(new RoomInventory(), new EmployeeInventory());
initializeDefaultObservers();
}
private void initializeDefaultObservers() {
orchestrator.subscribe(new EmailObserver());
orchestrator.subscribe(new CalendarObserver());
orchestrator.subscribe(new SlackObserver());
}
public static synchronized RoomBookingSystem getInstance() {
if (systemInstance == null)
systemInstance = new RoomBookingSystem();
return systemInstance;
}
public RoomBookingOrchestrator getOrchestrator() { return orchestrator; }
public void registerRoom(String roomName, String roomType, List<Integer> availableSlots) {
orchestrator.registerRoom(roomName, roomType, availableSlots);
}
public void registerEmployee(String employeeName, String department) {
orchestrator.registerEmployee(employeeName, department);
}
public void showAllRegisteredRooms() { orchestrator.showAllRegisteredRooms(); }
public void showAllRegisteredEmployees() { orchestrator.showAllRegisteredEmployees(); }
public void bookRoom(String employeeName, int totalAttendees, LocalTime start, LocalTime end) {
System.out.println("--- Booking Initiated ---");
orchestrator.bookRoom(employeeName, totalAttendees, start, end);
System.out.println("--- Booking Ended ---");
}
public void bookRoomRecurring(String employeeName, int totalAttendees, Recurrence recurrence) {
System.out.println("--- Recurring Booking Initiated ---");
orchestrator.bookRoom(employeeName, totalAttendees, recurrence);
System.out.println("--- Recurring Booking Ended ---");
}
public void viewSchedule() {
orchestrator.viewRoomSchedule();
orchestrator.viewEmployeeBookings();
}
}package RoomBookingSystem.orchestrator;
import RoomBookingSystem.model.*;
import RoomBookingSystem.repository.EmployeeInventory;
import RoomBookingSystem.repository.RoomInventory;
import RoomBookingSystem.strategy.*;
import RoomBookingSystem.observer.BookingObserver;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class RoomBookingOrchestrator {
private final RoomInventory roomInventory;
private final EmployeeInventory employeeInventory;
private RoomStrategy roomSelectionStrategy;
private RecurringRoomStrategy recurringRoomSelectionStrategy;
private final List<BookingObserver> bookingObservers;
private static final LocalTime BUSINESS_START = LocalTime.of(9, 0);
private static final LocalTime BUSINESS_END = LocalTime.of(19, 0);
public RoomBookingOrchestrator(RoomInventory roomInventory, EmployeeInventory employeeInventory) {
this.roomInventory = roomInventory;
this.employeeInventory = employeeInventory;
this.roomSelectionStrategy = new BestFitStrategy();
this.recurringRoomSelectionStrategy = new BestFitRecurringStrategy();
this.bookingObservers = new CopyOnWriteArrayList<>();
}
public synchronized void subscribe(BookingObserver observer) { bookingObservers.add(observer); }
public synchronized void unsubscribe(BookingObserver observer) { bookingObservers.remove(observer); }
public void setRoomSelectionStrategy(RoomStrategy s) { this.roomSelectionStrategy = s; }
public void setRecurringRoomSelectionStrategy(RecurringRoomStrategy s) { this.recurringRoomSelectionStrategy = s; }
// ── Registration ───────────────────────────────────────────────────────────
public void registerRoom(String roomName, String roomType, List<Integer> slots) {
if (roomInventory.getRoom(roomName + "_" + roomType) != null) {
System.out.println("Room " + roomName + " (" + roomType + ") already registered.");
return;
}
Room room = new Room(roomName, roomType);
roomInventory.addRoom(room);
System.out.println("Registered: " + roomName + " | " + roomType);
}
public void registerEmployee(String name, String dept) {
if (employeeInventory.getEmployeeByName(name) != null) {
System.out.println("Employee " + name + " already registered.");
return;
}
employeeInventory.addEmployee(new Employee(name, dept));
System.out.println("Registered: " + name + " | " + dept);
}
// ── Single Booking ─────────────────────────────────────────────────────────
public void bookRoom(String employeeName, int attendees, LocalTime start, LocalTime end) {
if (!validateBookingInputs(employeeName, attendees, start, end)) return;
Room room = findBestRoom(attendees, start, end);
Booking booking = null;
if (room != null) {
synchronized (room) { // single-lock: no deadlock
if (room.canBook(start, end))
booking = createBooking(room, 0, start, end, employeeName);
}
}
if (booking != null) {
printBookingConfirmation(booking);
bookingObservers.forEach(o -> o.onBookingCreated(booking));
} else {
System.out.println("Booking failed: no available room.");
}
}
// ── Recurring Booking ──────────────────────────────────────────────────────
public List<Booking> bookRoom(String employeeName, int attendees, Recurrence recurrence) {
List<Booking> bookings = new ArrayList<>();
if (!validateBookingInputs(employeeName, attendees, recurrence.getStart(), recurrence.getEnd()))
return bookings;
Room room = findBestRoom(attendees, recurrence);
if (room == null) {
System.out.println("Recurring booking failed: no suitable room for the full schedule.");
return bookings;
}
int currentDay = recurrence.getDayOfWeek();
int increment = switch (recurrence.getFrequencyType()) {
case DAILY -> 1;
case WEEKLY -> 7;
case BIWEEKLY -> 14;
case MONTHLY -> 30;
};
int occurrences = recurrence.getFrequencyType() == FrequencyType.DAILY
? recurrence.getNumberOfWeeks() * 7
: recurrence.getNumberOfWeeks();
synchronized (room) { // atomic: all-or-nothing
for (int i = 0; i < occurrences; i++) {
if (!room.canBookForDay(currentDay, recurrence.getStart(), recurrence.getEnd())) {
// Rollback all previous occurrences
bookings.forEach(b -> {
room.cancelBooking(b.getDay(), b.getStartTime());
Employee emp = employeeInventory.getEmployeeByName(employeeName);
if (emp != null) emp.removeBooking(b);
});
bookings.clear();
System.out.println("Recurring booking failed on day " + currentDay + ". Rolled back.");
return bookings;
}
Booking b = createBooking(room, currentDay, recurrence.getStart(), recurrence.getEnd(), employeeName);
bookings.add(b);
currentDay += increment;
}
}
System.out.println("Recurring booking confirmed: " + bookings.size() + " occurrences.");
bookingObservers.forEach(o -> o.onRecurringBookingCreated(bookings));
return bookings;
}
// ── Helpers ────────────────────────────────────────────────────────────────
private Booking createBooking(Room room, int day, LocalTime start, LocalTime end, String emp) {
Booking b = new Booking(room, start, end, emp, day);
if (room.bookSlots(day, start, end, b)) {
Employee employee = employeeInventory.getEmployeeByName(emp);
if (employee != null) employee.addBooking(b);
return b;
}
return null;
}
private Room findBestRoom(int attendees, LocalTime start, LocalTime end) {
return roomSelectionStrategy.selectRoom(new ArrayList<>(roomInventory.getAllRooms().values()), start, end);
}
private Room findBestRoom(int attendees, Recurrence r) {
return recurringRoomSelectionStrategy.selectRoom(new ArrayList<>(roomInventory.getAllRooms().values()), r);
}
private boolean validateBookingInputs(String emp, int attendees, LocalTime start, LocalTime end) {
if (attendees <= 0 || attendees > 30) { System.out.println("Invalid attendee count."); return false; }
if (start == null || end == null || !start.isBefore(end)) { System.out.println("Invalid time range."); return false; }
if (start.isBefore(BUSINESS_START) || end.isAfter(BUSINESS_END)) { System.out.println("Outside business hours."); return false; }
if (employeeInventory.getEmployeeByName(emp) == null) { System.out.println("Employee not found: " + emp); return false; }
return true;
}
private void printBookingConfirmation(Booking b) {
System.out.printf("Booked: %s | %s | %s–%s | ID: %s%n",
b.getRoom().getRoomName(), b.getEmployeeName(),
b.getStartTime(), b.getEndTime(), b.getBookingId());
}
public synchronized void viewRoomSchedule() { roomInventory.getAllRooms().values().forEach(Room::displayBookings); }
public synchronized void viewEmployeeBookings() { employeeInventory.getAllEmployees().values().forEach(Employee::displayBookings); }
public void showAllRegisteredRooms() { roomInventory.getAllRooms().forEach((k, v) -> System.out.println(k + " → " + v.getRoomName())); }
public void showAllRegisteredEmployees() { employeeInventory.getAllEmployees().forEach((k, v) -> System.out.println(k + " → " + v.getEmployeeName())); }
}Models#
package RoomBookingSystem.model;
import java.time.LocalTime;
import java.util.*;
public class Room {
private final String roomId;
private final String roomName;
private final RoomType roomType;
// day → TreeMap<startTime, Booking>; TreeMap gives O(log n) overlap detection
private final Map<Integer, TreeMap<LocalTime, Booking>> bookingsByDay = new HashMap<>();
public Room(String name, String type) {
this.roomId = name + "_" + type;
this.roomName = name;
this.roomType = RoomType.valueOf(type);
}
public String getRoomId() { return roomId; }
public String getRoomName() { return roomName; }
public RoomType getRoomType() { return roomType; }
/** O(log n) conflict check using predecessor / successor in the TreeMap. */
public synchronized boolean canBookForDay(int day, LocalTime start, LocalTime end) {
TreeMap<LocalTime, Booking> dayMap = bookingsByDay.get(day);
if (dayMap == null || dayMap.isEmpty()) return true;
Map.Entry<LocalTime, Booking> before = dayMap.floorEntry(start);
if (before != null && before.getValue().getEndTime().isAfter(start)) return false;
Map.Entry<LocalTime, Booking> after = dayMap.ceilingEntry(start);
if (after != null && after.getKey().isBefore(end)) return false;
return true;
}
/** Convenience for day-0 (single bookings). */
public synchronized boolean canBook(LocalTime start, LocalTime end) {
return canBookForDay(0, start, end);
}
/** Check every occurrence in a recurring schedule before committing. */
public synchronized boolean canBookRecurring(Recurrence r) {
int day = r.getDayOfWeek();
int incr = switch (r.getFrequencyType()) {
case DAILY -> 1; case WEEKLY -> 7; case BIWEEKLY -> 14; case MONTHLY -> 30;
};
int n = r.getFrequencyType() == FrequencyType.DAILY
? r.getNumberOfWeeks() * 7 : r.getNumberOfWeeks();
for (int i = 0; i < n; i++) {
if (!canBookForDay(day, r.getStart(), r.getEnd())) return false;
day += incr;
}
return true;
}
public synchronized boolean bookSlots(int day, LocalTime start, LocalTime end, Booking booking) {
if (!canBookForDay(day, start, end)) return false;
bookingsByDay.computeIfAbsent(day, d -> new TreeMap<>()).put(start, booking);
return true;
}
public synchronized void cancelBooking(int day, LocalTime start) {
TreeMap<LocalTime, Booking> m = bookingsByDay.get(day);
if (m != null) { m.remove(start); if (m.isEmpty()) bookingsByDay.remove(day); }
}
public synchronized void displayBookings() {
System.out.println("Room: " + roomName + " (" + roomType + ")");
if (bookingsByDay.isEmpty()) { System.out.println(" No bookings."); return; }
bookingsByDay.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> {
System.out.println(" Day " + e.getKey() + ":");
e.getValue().values().forEach(b -> System.out.printf(
" %s–%s %s [%s]%n",
b.getStartTime(), b.getEndTime(), b.getEmployeeName(), b.getBookingId()));
});
}
}package RoomBookingSystem.model;
import java.time.LocalTime;
import java.util.UUID;
public class Booking {
private final UUID bookingId;
private final Room room;
private final LocalTime startTime;
private final LocalTime endTime;
private final String employeeName;
private final int day;
public Booking(Room room, LocalTime start, LocalTime end, String emp, int day) {
this.bookingId = UUID.randomUUID();
this.room = room;
this.startTime = start;
this.endTime = end;
this.employeeName = emp;
this.day = day;
}
public UUID getBookingId() { return bookingId; }
public Room getRoom() { return room; }
public LocalTime getStartTime() { return startTime; }
public LocalTime getEndTime() { return endTime; }
public String getEmployeeName() { return employeeName; }
public int getDay() { return day; }
}package RoomBookingSystem.model;
import java.time.LocalTime;
import java.util.UUID;
public class Recurrence {
private final UUID recurrenceId;
private final int numberOfWeeks;
private final LocalTime start;
private final LocalTime end;
private final int dayOfWeek;
private final FrequencyType frequencyType;
private static final LocalTime BUSINESS_START = LocalTime.of(9, 0);
private static final LocalTime BUSINESS_END = LocalTime.of(19, 0);
/** Prefer Builder; this constructor exists for backward compatibility. */
public Recurrence(int weeks, LocalTime start, LocalTime end, int dayOfWeek, String frequency) {
this.recurrenceId = UUID.randomUUID();
this.numberOfWeeks = weeks;
this.start = start;
this.end = end;
this.dayOfWeek = dayOfWeek;
this.frequencyType = FrequencyType.valueOf(frequency);
}
private Recurrence(Builder b) {
this.recurrenceId = UUID.randomUUID();
this.numberOfWeeks = b.numberOfWeeks;
this.start = b.start;
this.end = b.end;
this.dayOfWeek = b.dayOfWeek;
this.frequencyType = b.frequencyType;
}
public int getNumberOfWeeks() { return numberOfWeeks; }
public LocalTime getStart() { return start; }
public LocalTime getEnd() { return end; }
public int getDayOfWeek() { return dayOfWeek; }
public FrequencyType getFrequencyType() { return frequencyType; }
// ── Builder ──────────────────────────────────────────────────────────────
public static class Builder {
private final int numberOfWeeks;
private final LocalTime start;
private final LocalTime end;
private final int dayOfWeek;
private FrequencyType frequencyType = FrequencyType.WEEKLY; // default
public Builder(int numberOfWeeks, LocalTime start, LocalTime end, int dayOfWeek) {
this.numberOfWeeks = numberOfWeeks;
this.start = start;
this.end = end;
this.dayOfWeek = dayOfWeek;
}
public Builder withFrequency(String frequency) {
this.frequencyType = FrequencyType.valueOf(frequency.toUpperCase());
return this;
}
public Builder withFrequency(FrequencyType ft) {
this.frequencyType = ft;
return this;
}
public Recurrence build() {
validate();
return new Recurrence(this);
}
private void validate() {
if (numberOfWeeks < 1 || numberOfWeeks > 52)
throw new IllegalArgumentException("Weeks must be 1–52, got: " + numberOfWeeks);
if (start == null || end == null || !start.isBefore(end))
throw new IllegalArgumentException("start must be non-null and before end");
if (start.isBefore(BUSINESS_START) || end.isAfter(BUSINESS_END))
throw new IllegalArgumentException("Times must be within business hours (09:00–19:00)");
if (dayOfWeek < 1 || dayOfWeek > 6)
throw new IllegalArgumentException("dayOfWeek must be 1–6, got: " + dayOfWeek);
}
}
}Strategy#
package RoomBookingSystem.strategy;
import RoomBookingSystem.model.Room;
import java.time.LocalTime;
import java.util.List;
public interface RoomStrategy {
Room selectRoom(List<Room> availableRooms, LocalTime start, LocalTime end);
}package RoomBookingSystem.strategy;
import RoomBookingSystem.model.Room;
import RoomBookingSystem.model.RoomType;
import java.time.LocalTime;
import java.util.List;
/** Prefers SMALL rooms to conserve large rooms for bigger meetings. */
public class BestFitStrategy implements RoomStrategy {
@Override
public Room selectRoom(List<Room> rooms, LocalTime start, LocalTime end) {
for (Room r : rooms)
if (r.getRoomType() == RoomType.SMALL && r.canBook(start, end)) return r;
for (Room r : rooms)
if (r.getRoomType() == RoomType.LARGE && r.canBook(start, end)) return r;
return null;
}
}Observer#
package RoomBookingSystem.observer;
import RoomBookingSystem.model.Booking;
import java.util.List;
public interface BookingObserver {
void onBookingCreated(Booking booking);
void onRecurringBookingCreated(List<Booking> bookings);
void onBookingCancelled(Booking booking);
void onRecurringBookingCancelled(List<Booking> bookings);
}package RoomBookingSystem.observer;
import RoomBookingSystem.model.Booking;
import java.util.List;
public class EmailObserver implements BookingObserver {
@Override
public void onBookingCreated(Booking b) {
sendEmail(b.getEmployeeName(), "Room Booking Confirmation",
String.format("Room: %s | Time: %s–%s | Day: %d | ID: %s",
b.getRoom().getRoomName(), b.getStartTime(), b.getEndTime(),
b.getDay(), b.getBookingId()));
}
@Override
public void onRecurringBookingCreated(List<Booking> bookings) {
if (bookings.isEmpty()) return;
Booking first = bookings.get(0);
sendEmail(first.getEmployeeName(), "Recurring Booking Confirmed",
"Room: " + first.getRoom().getRoomName() + " | Occurrences: " + bookings.size());
}
@Override
public void onBookingCancelled(Booking b) {
sendEmail(b.getEmployeeName(), "Booking Cancelled", "ID: " + b.getBookingId());
}
@Override
public void onRecurringBookingCancelled(List<Booking> bookings) {
if (bookings.isEmpty()) return;
sendEmail(bookings.get(0).getEmployeeName(), "Recurring Booking Cancelled",
bookings.size() + " occurrence(s) cancelled.");
}
private void sendEmail(String to, String subject, String body) {
System.out.println("[EMAIL] To: " + to + " | Subject: " + subject);
System.out.println(" " + body);
// Production: use SMTP / SendGrid / SES here
}
}Simulation#
import RoomBookingSystem.service.RoomBookingSystem;
import RoomBookingSystem.model.Recurrence;
import java.time.LocalTime;
import java.util.List;
public class RoomBookingSystemSimulation {
public static void main(String[] args) {
RoomBookingSystem system = RoomBookingSystem.getInstance();
// Register rooms
List<Integer> slots = List.of(1,2,3,4,5,6,7,8,9,10);
system.registerRoom("Room A", "SMALL", slots);
system.registerRoom("Room B", "LARGE", slots);
system.registerRoom("Room A", "SMALL", slots); // duplicate → rejected
system.showAllRegisteredRooms();
// Register employees
system.registerEmployee("Alice", "Marketing");
system.registerEmployee("Bob", "Sales");
system.registerEmployee("David", "IT");
system.registerEmployee("Bob", "Sales"); // duplicate → rejected
// Single bookings
system.bookRoom("Alice", 7, LocalTime.of(10, 0), LocalTime.of(10, 30));
system.bookRoom("Bob", 12, LocalTime.of(9, 0), LocalTime.of(10, 30));
system.bookRoom("Charlie", 3, LocalTime.of(9, 0), LocalTime.of(10, 30)); // fails: not registered
system.bookRoom("Alice", 7, LocalTime.of(10, 0), LocalTime.of(13, 0)); // conflict → rejected
// Recurring bookings via Builder
var weekly = new Recurrence.Builder(3, LocalTime.of(11, 0), LocalTime.of(12, 0), 3)
.withFrequency("WEEKLY").build();
system.bookRoomRecurring("David", 5, weekly);
var daily = new Recurrence.Builder(2, LocalTime.of(13, 0), LocalTime.of(14, 0), 1)
.withFrequency("DAILY").build();
system.bookRoomRecurring("Bob", 13, daily);
system.viewSchedule();
}
}
Class Diagram#
Extendible — Follow Ups#
Add a cancellation flow with undo#
Implement cancelBooking(UUID bookingId) on RoomBookingSystem. The Orchestrator looks up the booking across all rooms (or a separate BookingRegistry), removes it from the room's TreeMap and the employee's list, and fires onBookingCancelled() on all observers. For recurring series, introduce a SeriesId that groups individual Booking objects so the whole series can be cancelled atomically.
Support waitlisting and automatic rebooking#
When a room is unavailable, enqueue the request in a per-slot waiting list (priority queue ordered by request timestamp or employee seniority). When any booking is cancelled, the Orchestrator checks the waitlist for that slot and automatically re-books the next eligible request, firing observer events for both the cancellation and the new booking.
Add room-specific features (projector, whiteboard, VC equipment)#
Extend Room with a Set<Amenity> (enum: PROJECTOR, WHITEBOARD, VIDEO_CONFERENCE). Add an amenities parameter to the booking request. Update RoomStrategy to filter rooms by required amenities before selecting. Existing strategies get an amenity-aware wrapper or a new AmenityFilterStrategy that chains with the existing one.
Persist bookings to a database#
Replace the in-memory TreeMap<LocalTime, Booking> inside Room with a thin DAO layer. On startup the system hydrates room and employee state from the database. On every successful bookSlots() call, persist asynchronously via a write-behind queue (prevents the DB write from blocking the synchronized booking path). viewSchedule() reads from the in-memory state (cache-first); a daily reconciliation job refreshes it from the DB to catch any drift.