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.

August 21, 2026·20 min read

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#

EntityResponsibility
RoomBookingSystemSingleton + Facade — single entry point; initializes default observers; delegates to Orchestrator
RoomBookingOrchestratorCore business logic — validates inputs, finds rooms via strategy, creates bookings, notifies observers
RoomInventoryConcurrentHashMap-backed repository for rooms; returns safe snapshots
EmployeeInventoryConcurrentHashMap-backed repository for employees
RoomManages bookings per day via TreeMap<LocalTime, Booking>; O(log n) conflict detection; all mutating methods synchronized
EmployeeHolds employee metadata and a CopyOnWriteArrayList of bookings
BookingImmutable record of one booking: room, start/end time, day index, employee name, UUID
RecurrenceImmutable value object for recurring schedules; built via inner Builder with validation
RoomStrategyInterface for single-booking room selection
RecurringRoomStrategyInterface for recurring-booking room selection
BookingObserverInterface for notification subscribers
RoomTypeEnum: SMALL, LARGE
FrequencyTypeEnum: 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:

java
// 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:

java
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#

java
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();
    }
}

Models#

java
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()));
                });
    }
}

Strategy#

java
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);
}

Observer#

java
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);
}

Simulation#

java
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.