Restaurant Booking System
Difficulty: Intermediate-Advanced Patterns: Strategy, State, Observer, Repository Asked at: Zomato, Swiggy, OpenTable, Amazon, Google
Functional Requirements
- Table management โ register tables with capacity and type (Indoor, Outdoor, VIP)
- Time-slot reservations โ book tables with conflict detection for overlapping slots
- Waitlist management โ FIFO waitlist per time slot when no tables available
- Auto-promotion โ cancellation triggers automatic waitlist-to-booking promotion
- Booking lifecycle โ state machine: CONFIRMED โ SEATED โ COMPLETED (or CANCELLED)
Non-Functional Requirements
- Thread-safety โ concurrent booking attempts cannot double-book a table
- Conflict detection โ overlap-based, not just exact time match
- Extensibility โ swappable table assignment strategies at runtime
- State integrity โ only valid state transitions allowed
Core Entities
| Entity | Description |
|---|---|
TableType |
Enum โ INDOOR, OUTDOOR, VIP |
BookingStatus |
Enum โ CONFIRMED, SEATED, COMPLETED, CANCELLED |
TimeSlot |
Date + start/end time with overlap detection |
Table |
Immutable โ id, capacity, type |
Booking |
Customer reservation with state machine transitions |
WaitlistEntry |
FIFO queue entry โ customer, party size, time slot, timestamp |
TableAssignmentStrategy |
Interface โ pick the best table from available list |
FirstFitStrategy |
Assigns the first available table that fits the party |
BestFitStrategy |
Assigns the smallest available table that fits (minimizes waste) |
BookingService |
Orchestrates booking, cancellation, waitlist promotion with lock |
Class Diagram
classDiagram
class Restaurant {
-String id
-String name
-List tables
+addTable(int capacity, TableType type) Table
+getTables() List
}
class Table {
-String id
-int capacity
-TableType tableType
+getId() String
+getCapacity() int
+getTableType() TableType
}
class TableType {
<<enumeration>>
INDOOR
OUTDOOR
VIP
}
class TimeSlot {
-LocalDate date
-LocalTime startTime
-LocalTime endTime
+overlapsWith(TimeSlot other) boolean
}
class Booking {
-String id
-String customerName
-int partySize
-Table table
-TimeSlot timeSlot
-BookingStatus status
+confirm()
+seat()
+complete()
+cancel()
}
class BookingStatus {
<<enumeration>>
CONFIRMED
SEATED
COMPLETED
CANCELLED
}
class WaitlistEntry {
-String customerName
-int partySize
-TimeSlot timeSlot
-LocalDateTime addedAt
}
class TableAssignmentStrategy {
<<interface>>
+assignTable(List availableTables, int partySize) Table
}
class FirstFitStrategy {
+assignTable(List availableTables, int partySize) Table
}
class BestFitStrategy {
+assignTable(List availableTables, int partySize) Table
}
class BookingService {
-TableRepository tableRepo
-BookingRepository bookingRepo
-WaitlistRepository waitlistRepo
-TableAssignmentStrategy strategy
-ReentrantLock lock
+bookTable(String customer, int partySize, TimeSlot slot) BookingResult
+cancelBooking(String bookingId)
+getAvailableTables(TimeSlot slot, int partySize) List
+seatGuests(String bookingId)
+completeBooking(String bookingId)
+getWaitlist(TimeSlot slot) List
}
class TableRepository {
<<interface>>
+save(Table)
+findById(String) Table
+findAll() List
}
class BookingRepository {
<<interface>>
+save(Booking)
+findById(String) Booking
+findByTableAndSlot(String tableId, TimeSlot slot) List
}
class WaitlistRepository {
<<interface>>
+add(WaitlistEntry)
+getBySlot(TimeSlot) List
+removeFirst(TimeSlot) WaitlistEntry
}
BookingService --> TableRepository
BookingService --> BookingRepository
BookingService --> WaitlistRepository
BookingService --> TableAssignmentStrategy
TableAssignmentStrategy <|.. FirstFitStrategy
TableAssignmentStrategy <|.. BestFitStrategy
Restaurant --> Table
Table --> TableType
Booking --> Table
Booking --> TimeSlot
Booking --> BookingStatus
BookingRepository --> Booking
WaitlistRepository --> WaitlistEntry
WaitlistEntry --> TimeSlot
Design Patterns
| Pattern | Where | Why |
|---|---|---|
| Strategy | TableAssignmentStrategy with FirstFit/BestFit |
Swap table selection algorithm at runtime without changing BookingService |
| State | Booking lifecycle transitions |
Strict state machine โ CONFIRMED โ SEATED โ COMPLETED or CANCELLED. Invalid transitions throw exceptions |
| Observer | Waitlist auto-promotion on cancellation | Cancellation implicitly โnotifiesโ the waitlist, promoting the next eligible party |
| Repository | Table/Booking/Waitlist repositories | Abstract storage behind interfaces โ swap InMemory for database-backed without touching business logic |
How It All Fits Together
Hereโs what happens when a customer wants to book a table:
- Client calls
bookTable(customerName, partySize, timeSlot)on BookingService - Service acquires the lock to prevent race conditions
- Queries all tables, filters to those with sufficient capacity and no overlapping bookings
- Delegates to the TableAssignmentStrategy (FirstFit or BestFit) to pick the best table
- If a table is found: creates a Booking in CONFIRMED state, stores it, and returns
- If no table is available: creates a WaitlistEntry, adds to FIFO queue for that slot
- On cancellation: booking transitions to CANCELLED, then
promoteFromWaitlist()automatically tries to seat the next waitlisted party
๐ก The lock ensures that between checking availability and creating the booking, no other thread can sneak in and grab the same table. This is the critical section that prevents double-booking.
Complete Code
Enums and TimeSlot
TableType and BookingStatus are simple enums. TimeSlot is the key domain primitive โ it holds a date plus start/end time, and its overlapsWith() method is the foundation of conflict detection. Two slots conflict if they share the same date and their time ranges intersect.
import java.time.*;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
// --- Enums ---
enum TableType { INDOOR, OUTDOOR, VIP }
enum BookingStatus { CONFIRMED, SEATED, COMPLETED, CANCELLED }
// --- TimeSlot ---
class TimeSlot {
private final LocalDate date;
private final LocalTime startTime;
private final LocalTime endTime;
public TimeSlot(LocalDate date, LocalTime startTime, LocalTime endTime) {
this.date = date;
this.startTime = startTime;
this.endTime = endTime;
}
public boolean overlapsWith(TimeSlot other) {
if (!this.date.equals(other.date)) return false;
return this.startTime.isBefore(other.endTime) && other.startTime.isBefore(this.endTime);
}
public LocalDate getDate() { return date; }
public LocalTime getStartTime() { return startTime; }
public LocalTime getEndTime() { return endTime; }
@Override
public String toString() {
return date + " " + startTime + "-" + endTime;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TimeSlot)) return false;
TimeSlot t = (TimeSlot) o;
return date.equals(t.date) && startTime.equals(t.startTime) && endTime.equals(t.endTime);
}
@Override
public int hashCode() { return Objects.hash(date, startTime, endTime); }
}
import threading
from datetime import date, time, datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import List, Optional, Protocol
# --- Enums ---
class TableType(Enum):
INDOOR = "INDOOR"
OUTDOOR = "OUTDOOR"
VIP = "VIP"
class BookingStatus(Enum):
CONFIRMED = "CONFIRMED"
SEATED = "SEATED"
COMPLETED = "COMPLETED"
CANCELLED = "CANCELLED"
# --- TimeSlot ---
@dataclass
class TimeSlot:
date: date
start_time: time
end_time: time
def overlaps_with(self, other: "TimeSlot") -> bool:
if self.date != other.date:
return False
return self.start_time < other.end_time and other.start_time < self.end_time
def __hash__(self):
return hash((self.date, self.start_time, self.end_time))
def __eq__(self, other):
if not isinstance(other, TimeSlot):
return False
return (self.date == other.date and self.start_time == other.start_time
and self.end_time == other.end_time)
def __str__(self):
return f"{self.date} {self.start_time}-{self.end_time}"
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <list>
#include <algorithm>
#include <mutex>
#include <memory>
#include <stdexcept>
#include <sstream>
#include <chrono>
// --- Enums ---
enum class TableType { INDOOR, OUTDOOR, VIP };
enum class BookingStatus { CONFIRMED, SEATED, COMPLETED, CANCELLED };
std::string tableTypeStr(TableType t) {
switch (t) {
case TableType::INDOOR: return "INDOOR";
case TableType::OUTDOOR: return "OUTDOOR";
case TableType::VIP: return "VIP";
}
return "";
}
std::string bookingStatusStr(BookingStatus s) {
switch (s) {
case BookingStatus::CONFIRMED: return "CONFIRMED";
case BookingStatus::SEATED: return "SEATED";
case BookingStatus::COMPLETED: return "COMPLETED";
case BookingStatus::CANCELLED: return "CANCELLED";
}
return "";
}
// --- TimeSlot ---
struct TimeSlot {
int year, month, day;
int startHour, startMin;
int endHour, endMin;
bool overlapsWith(const TimeSlot& other) const {
if (year != other.year || month != other.month || day != other.day) return false;
int thisStart = startHour * 60 + startMin;
int thisEnd = endHour * 60 + endMin;
int otherStart = other.startHour * 60 + other.startMin;
int otherEnd = other.endHour * 60 + other.endMin;
return thisStart < otherEnd && otherStart < thisEnd;
}
bool operator==(const TimeSlot& other) const {
return year == other.year && month == other.month && day == other.day
&& startHour == other.startHour && startMin == other.startMin
&& endHour == other.endHour && endMin == other.endMin;
}
bool operator<(const TimeSlot& other) const {
if (year != other.year) return year < other.year;
if (month != other.month) return month < other.month;
if (day != other.day) return day < other.day;
if (startHour != other.startHour) return startHour < other.startHour;
return startMin < other.startMin;
}
std::string toString() const {
std::ostringstream oss;
oss << year << "-" << month << "-" << day << " "
<< startHour << ":" << (startMin < 10 ? "0" : "") << startMin
<< "-" << endHour << ":" << (endMin < 10 ? "0" : "") << endMin;
return oss.str();
}
};
// --- Restaurant Booking System (JavaScript) ---
// --- Enums ---
const TableType = Object.freeze({ INDOOR: "INDOOR", OUTDOOR: "OUTDOOR", VIP: "VIP" });
const BookingStatus = Object.freeze({
CONFIRMED: "CONFIRMED", SEATED: "SEATED", COMPLETED: "COMPLETED", CANCELLED: "CANCELLED"
});
// --- TimeSlot ---
class TimeSlot {
constructor(date, startTime, endTime) {
this.date = date; // "YYYY-MM-DD"
this.startTime = startTime; // "HH:MM"
this.endTime = endTime; // "HH:MM"
}
overlapsWith(other) {
if (this.date !== other.date) return false;
return this.startTime < other.endTime && other.startTime < this.endTime;
}
key() { return `${this.date}_${this.startTime}_${this.endTime}`; }
toString() { return `${this.date} ${this.startTime}-${this.endTime}`; }
}
Table and Booking
Table is a simple immutable entity โ id, capacity, and type. Booking is where the state machine lives. Each booking starts in CONFIRMED and can only transition forward: CONFIRMED โ SEATED โ COMPLETED, or CONFIRMED โ CANCELLED. Any invalid transition throws an exception.
๐ก State integrity: the Booking class enforces valid transitions internally rather than relying on the service layer to check. This means even if a bug in BookingService calls complete() on a CONFIRMED booking, it will be caught immediately. Defense in depth.
class Table {
private final String id;
private final int capacity;
private final TableType tableType;
public Table(String id, int capacity, TableType tableType) {
this.id = id;
this.capacity = capacity;
this.tableType = tableType;
}
public String getId() { return id; }
public int getCapacity() { return capacity; }
public TableType getTableType() { return tableType; }
@Override
public String toString() {
return "Table-" + id + "(" + tableType + ", seats:" + capacity + ")";
}
}
class Booking {
private final String id;
private final String customerName;
private final int partySize;
private final Table table;
private final TimeSlot timeSlot;
private BookingStatus status;
public Booking(String id, String customerName, int partySize, Table table, TimeSlot timeSlot) {
this.id = id;
this.customerName = customerName;
this.partySize = partySize;
this.table = table;
this.timeSlot = timeSlot;
this.status = BookingStatus.CONFIRMED;
}
public void seat() {
if (status != BookingStatus.CONFIRMED)
throw new IllegalStateException("Can only seat from CONFIRMED state. Current: " + status);
this.status = BookingStatus.SEATED;
}
public void complete() {
if (status != BookingStatus.SEATED)
throw new IllegalStateException("Can only complete from SEATED state. Current: " + status);
this.status = BookingStatus.COMPLETED;
}
public void cancel() {
if (status != BookingStatus.CONFIRMED)
throw new IllegalStateException("Can only cancel from CONFIRMED state. Current: " + status);
this.status = BookingStatus.CANCELLED;
}
public String getId() { return id; }
public String getCustomerName() { return customerName; }
public int getPartySize() { return partySize; }
public Table getTable() { return table; }
public TimeSlot getTimeSlot() { return timeSlot; }
public BookingStatus getStatus() { return status; }
@Override
public String toString() {
return "Booking[" + id + "] " + customerName + " party:" + partySize +
" " + table + " " + timeSlot + " status:" + status;
}
}
@dataclass
class Table:
id: str
capacity: int
table_type: TableType
def __str__(self):
return f"Table-{self.id}({self.table_type.value}, seats:{self.capacity})"
@dataclass
class Booking:
id: str
customer_name: str
party_size: int
table: Table
time_slot: TimeSlot
status: BookingStatus = BookingStatus.CONFIRMED
def seat(self):
if self.status != BookingStatus.CONFIRMED:
raise ValueError(f"Can only seat from CONFIRMED. Current: {self.status.value}")
self.status = BookingStatus.SEATED
def complete(self):
if self.status != BookingStatus.SEATED:
raise ValueError(f"Can only complete from SEATED. Current: {self.status.value}")
self.status = BookingStatus.COMPLETED
def cancel(self):
if self.status != BookingStatus.CONFIRMED:
raise ValueError(f"Can only cancel from CONFIRMED. Current: {self.status.value}")
self.status = BookingStatus.CANCELLED
def __str__(self):
return (f"Booking[{self.id}] {self.customer_name} party:{self.party_size} "
f"{self.table} {self.time_slot} status:{self.status.value}")
struct Table {
std::string id;
int capacity;
TableType tableType;
std::string toString() const {
return "Table-" + id + "(" + tableTypeStr(tableType) + ", seats:" + std::to_string(capacity) + ")";
}
};
struct Booking {
std::string id;
std::string customerName;
int partySize;
std::shared_ptr<Table> table;
TimeSlot timeSlot;
BookingStatus status = BookingStatus::CONFIRMED;
void seat() {
if (status != BookingStatus::CONFIRMED)
throw std::runtime_error("Can only seat from CONFIRMED. Current: " + bookingStatusStr(status));
status = BookingStatus::SEATED;
}
void complete() {
if (status != BookingStatus::SEATED)
throw std::runtime_error("Can only complete from SEATED. Current: " + bookingStatusStr(status));
status = BookingStatus::COMPLETED;
}
void cancel() {
if (status != BookingStatus::CONFIRMED)
throw std::runtime_error("Can only cancel from CONFIRMED. Current: " + bookingStatusStr(status));
status = BookingStatus::CANCELLED;
}
std::string toString() const {
return "Booking[" + id + "] " + customerName + " party:" + std::to_string(partySize)
+ " " + table->toString() + " " + timeSlot.toString() + " status:" + bookingStatusStr(status);
}
};
class Table {
constructor(id, capacity, tableType) {
this.id = id;
this.capacity = capacity;
this.tableType = tableType;
}
toString() { return `Table-${this.id}(${this.tableType}, seats:${this.capacity})`; }
}
class Booking {
constructor(id, customerName, partySize, table, timeSlot) {
this.id = id;
this.customerName = customerName;
this.partySize = partySize;
this.table = table;
this.timeSlot = timeSlot;
this.status = BookingStatus.CONFIRMED;
}
seat() {
if (this.status !== BookingStatus.CONFIRMED)
throw new Error(`Can only seat from CONFIRMED. Current: ${this.status}`);
this.status = BookingStatus.SEATED;
}
complete() {
if (this.status !== BookingStatus.SEATED)
throw new Error(`Can only complete from SEATED. Current: ${this.status}`);
this.status = BookingStatus.COMPLETED;
}
cancel() {
if (this.status !== BookingStatus.CONFIRMED)
throw new Error(`Can only cancel from CONFIRMED. Current: ${this.status}`);
this.status = BookingStatus.CANCELLED;
}
toString() {
return `Booking[${this.id}] ${this.customerName} party:${this.partySize} ` +
`${this.table.toString()} ${this.timeSlot.toString()} status:${this.status}`;
}
}
WaitlistEntry
A simple value object representing a customer waiting for a table in a specific time slot. Entries are stored in FIFO order per slot โ the first person to join the waitlist gets promoted first when a cancellation frees up a table.
class WaitlistEntry {
private final String customerName;
private final int partySize;
private final TimeSlot timeSlot;
private final LocalDateTime addedAt;
public WaitlistEntry(String customerName, int partySize, TimeSlot timeSlot) {
this.customerName = customerName;
this.partySize = partySize;
this.timeSlot = timeSlot;
this.addedAt = LocalDateTime.now();
}
public String getCustomerName() { return customerName; }
public int getPartySize() { return partySize; }
public TimeSlot getTimeSlot() { return timeSlot; }
public LocalDateTime getAddedAt() { return addedAt; }
@Override
public String toString() {
return "Waitlist[" + customerName + " party:" + partySize + " slot:" + timeSlot + "]";
}
}
@dataclass
class WaitlistEntry:
customer_name: str
party_size: int
time_slot: TimeSlot
added_at: datetime = field(default_factory=datetime.now)
def __str__(self):
return f"Waitlist[{self.customer_name} party:{self.party_size} slot:{self.time_slot}]"
struct WaitlistEntry {
std::string customerName;
int partySize;
TimeSlot timeSlot;
std::string toString() const {
return "Waitlist[" + customerName + " party:" + std::to_string(partySize)
+ " slot:" + timeSlot.toString() + "]";
}
};
class WaitlistEntry {
constructor(customerName, partySize, timeSlot) {
this.customerName = customerName;
this.partySize = partySize;
this.timeSlot = timeSlot;
this.addedAt = new Date();
}
toString() {
return `Waitlist[${this.customerName} party:${this.partySize} slot:${this.timeSlot.toString()}]`;
}
}
Table Assignment Strategy
The Strategy interface defines how a table is selected from a list of available candidates. FirstFitStrategy picks the first table with enough capacity โ simple and fast. BestFitStrategy picks the smallest table that fits โ minimizes wasted seats so larger tables remain available for bigger parties.
๐ก Strategy pattern = define a family of algorithms, encapsulate each one, and make them interchangeable. BookingService delegates table selection to whichever strategy is currently set. You can swap strategies at runtime with setStrategy() โ no code changes, no if/else branching.
// --- Strategy Pattern ---
interface TableAssignmentStrategy {
Table assignTable(List<Table> availableTables, int partySize);
}
class FirstFitStrategy implements TableAssignmentStrategy {
@Override
public Table assignTable(List<Table> availableTables, int partySize) {
return availableTables.stream()
.filter(t -> t.getCapacity() >= partySize)
.findFirst()
.orElse(null);
}
}
class BestFitStrategy implements TableAssignmentStrategy {
@Override
public Table assignTable(List<Table> availableTables, int partySize) {
return availableTables.stream()
.filter(t -> t.getCapacity() >= partySize)
.min(Comparator.comparingInt(Table::getCapacity))
.orElse(null);
}
}
# --- Strategy Pattern ---
class TableAssignmentStrategy(Protocol):
def assign_table(self, available_tables: List[Table], party_size: int) -> Optional[Table]:
...
class FirstFitStrategy:
def assign_table(self, available_tables: List[Table], party_size: int) -> Optional[Table]:
for table in available_tables:
if table.capacity >= party_size:
return table
return None
class BestFitStrategy:
def assign_table(self, available_tables: List[Table], party_size: int) -> Optional[Table]:
fitting = [t for t in available_tables if t.capacity >= party_size]
if not fitting:
return None
return min(fitting, key=lambda t: t.capacity)
// --- Strategy Pattern ---
class TableAssignmentStrategy {
public:
virtual ~TableAssignmentStrategy() = default;
virtual std::shared_ptr<Table> assignTable(
const std::vector<std::shared_ptr<Table>>& available, int partySize) = 0;
};
class FirstFitStrategy : public TableAssignmentStrategy {
public:
std::shared_ptr<Table> assignTable(
const std::vector<std::shared_ptr<Table>>& available, int partySize) override {
for (auto& t : available) {
if (t->capacity >= partySize) return t;
}
return nullptr;
}
};
class BestFitStrategy : public TableAssignmentStrategy {
public:
std::shared_ptr<Table> assignTable(
const std::vector<std::shared_ptr<Table>>& available, int partySize) override {
std::shared_ptr<Table> best = nullptr;
for (auto& t : available) {
if (t->capacity >= partySize) {
if (!best || t->capacity < best->capacity) {
best = t;
}
}
}
return best;
}
};
// --- Strategy Pattern ---
class FirstFitStrategy {
assignTable(availableTables, partySize) {
return availableTables.find(t => t.capacity >= partySize) || null;
}
}
class BestFitStrategy {
assignTable(availableTables, partySize) {
const fitting = availableTables
.filter(t => t.capacity >= partySize)
.sort((a, b) => a.capacity - b.capacity);
return fitting.length > 0 ? fitting[0] : null;
}
}
Repositories
Storage is abstracted behind repository interfaces. The InMemory implementations use simple maps and lists โ but the same interface could be backed by PostgreSQL, DynamoDB, or any persistent store without changing a single line in BookingService.
// --- Repositories ---
interface TableRepository {
void save(Table table);
Table findById(String id);
List<Table> findAll();
}
interface BookingRepository {
void save(Booking booking);
Booking findById(String id);
List<Booking> findByTableAndOverlappingSlot(String tableId, TimeSlot slot);
List<Booking> findAll();
}
interface WaitlistRepository {
void add(WaitlistEntry entry);
List<WaitlistEntry> getBySlot(TimeSlot slot);
WaitlistEntry removeFirst(TimeSlot slot);
}
class InMemoryTableRepository implements TableRepository {
private final Map<String, Table> tables = new HashMap<>();
@Override
public void save(Table table) { tables.put(table.getId(), table); }
@Override
public Table findById(String id) { return tables.get(id); }
@Override
public List<Table> findAll() { return new ArrayList<>(tables.values()); }
}
class InMemoryBookingRepository implements BookingRepository {
private final List<Booking> bookings = new ArrayList<>();
@Override
public void save(Booking booking) { bookings.add(booking); }
@Override
public Booking findById(String id) {
return bookings.stream().filter(b -> b.getId().equals(id)).findFirst().orElse(null);
}
@Override
public List<Booking> findByTableAndOverlappingSlot(String tableId, TimeSlot slot) {
return bookings.stream()
.filter(b -> b.getTable().getId().equals(tableId))
.filter(b -> b.getStatus() == BookingStatus.CONFIRMED || b.getStatus() == BookingStatus.SEATED)
.filter(b -> b.getTimeSlot().overlapsWith(slot))
.collect(Collectors.toList());
}
@Override
public List<Booking> findAll() { return new ArrayList<>(bookings); }
}
class InMemoryWaitlistRepository implements WaitlistRepository {
private final Map<TimeSlot, LinkedList<WaitlistEntry>> waitlist = new HashMap<>();
@Override
public void add(WaitlistEntry entry) {
waitlist.computeIfAbsent(entry.getTimeSlot(), k -> new LinkedList<>()).add(entry);
}
@Override
public List<WaitlistEntry> getBySlot(TimeSlot slot) {
return waitlist.getOrDefault(slot, new LinkedList<>());
}
@Override
public WaitlistEntry removeFirst(TimeSlot slot) {
LinkedList<WaitlistEntry> list = waitlist.get(slot);
if (list == null || list.isEmpty()) return null;
return list.removeFirst();
}
}
# --- Repositories ---
class InMemoryTableRepository:
def __init__(self):
self._tables: dict[str, Table] = {}
def save(self, table: Table):
self._tables[table.id] = table
def find_by_id(self, table_id: str) -> Optional[Table]:
return self._tables.get(table_id)
def find_all(self) -> List[Table]:
return list(self._tables.values())
class InMemoryBookingRepository:
def __init__(self):
self._bookings: List[Booking] = []
def save(self, booking: Booking):
self._bookings.append(booking)
def find_by_id(self, booking_id: str) -> Optional[Booking]:
for b in self._bookings:
if b.id == booking_id:
return b
return None
def find_by_table_and_overlapping_slot(self, table_id: str, slot: TimeSlot) -> List[Booking]:
return [
b for b in self._bookings
if b.table.id == table_id
and b.status in (BookingStatus.CONFIRMED, BookingStatus.SEATED)
and b.time_slot.overlaps_with(slot)
]
class InMemoryWaitlistRepository:
def __init__(self):
self._waitlist: dict[TimeSlot, list[WaitlistEntry]] = {}
def add(self, entry: WaitlistEntry):
self._waitlist.setdefault(entry.time_slot, []).append(entry)
def get_by_slot(self, slot: TimeSlot) -> List[WaitlistEntry]:
return self._waitlist.get(slot, [])
def remove_first(self, slot: TimeSlot) -> Optional[WaitlistEntry]:
entries = self._waitlist.get(slot, [])
if not entries:
return None
return entries.pop(0)
// Note: In C++, the BookingService class below acts as its own repository
// using internal vectors and maps. No separate repository classes needed
// for this in-memory implementation.
// Note: In JavaScript, the BookingService class below acts as its own repository
// using internal arrays and Maps. No separate repository classes needed
// for this in-memory implementation.
BookingService
The main orchestrator. It holds references to all repositories, the current assignment strategy, and a lock for thread safety. The bookTable() method is the heart of the system โ it acquires the lock, finds available tables, delegates to the strategy, and either confirms a booking or adds the customer to the waitlist.
๐ก Auto-promotion: when cancelBooking() is called, it doesnโt just cancel โ it immediately calls promoteFromWaitlist() to try seating the next person in the FIFO queue. This is the Observer pattern in action: cancellation implicitly โnotifiesโ the waitlist. The customer who was first in line gets automatically promoted without any manual intervention.
// --- Booking Result ---
class BookingResult {
private final Booking booking;
private final boolean waitlisted;
private final WaitlistEntry waitlistEntry;
private BookingResult(Booking booking, boolean waitlisted, WaitlistEntry waitlistEntry) {
this.booking = booking;
this.waitlisted = waitlisted;
this.waitlistEntry = waitlistEntry;
}
public static BookingResult confirmed(Booking booking) {
return new BookingResult(booking, false, null);
}
public static BookingResult waitlisted(WaitlistEntry entry) {
return new BookingResult(null, true, entry);
}
public Booking getBooking() { return booking; }
public boolean isWaitlisted() { return waitlisted; }
public WaitlistEntry getWaitlistEntry() { return waitlistEntry; }
@Override
public String toString() {
if (waitlisted) return "WAITLISTED: " + waitlistEntry;
return "CONFIRMED: " + booking;
}
}
// --- Booking Service ---
class BookingService {
private final TableRepository tableRepo;
private final BookingRepository bookingRepo;
private final WaitlistRepository waitlistRepo;
private TableAssignmentStrategy strategy;
private final ReentrantLock lock = new ReentrantLock();
private int bookingCounter = 0;
private int tableCounter = 0;
public BookingService(TableAssignmentStrategy strategy) {
this.tableRepo = new InMemoryTableRepository();
this.bookingRepo = new InMemoryBookingRepository();
this.waitlistRepo = new InMemoryWaitlistRepository();
this.strategy = strategy;
}
public void setStrategy(TableAssignmentStrategy strategy) {
this.strategy = strategy;
}
public Table addTable(int capacity, TableType type) {
Table table = new Table("T" + (++tableCounter), capacity, type);
tableRepo.save(table);
return table;
}
public List<Table> getAvailableTables(TimeSlot slot, int partySize) {
return tableRepo.findAll().stream()
.filter(t -> t.getCapacity() >= partySize)
.filter(t -> bookingRepo.findByTableAndOverlappingSlot(t.getId(), slot).isEmpty())
.collect(Collectors.toList());
}
public BookingResult bookTable(String customerName, int partySize, TimeSlot slot) {
lock.lock();
try {
List<Table> available = getAvailableTables(slot, partySize);
Table assigned = strategy.assignTable(available, partySize);
if (assigned != null) {
Booking booking = new Booking("B" + (++bookingCounter), customerName,
partySize, assigned, slot);
bookingRepo.save(booking);
System.out.println(" >> Booked: " + booking);
return BookingResult.confirmed(booking);
} else {
WaitlistEntry entry = new WaitlistEntry(customerName, partySize, slot);
waitlistRepo.add(entry);
System.out.println(" >> Waitlisted: " + entry);
return BookingResult.waitlisted(entry);
}
} finally {
lock.unlock();
}
}
public void cancelBooking(String bookingId) {
lock.lock();
try {
Booking booking = bookingRepo.findById(bookingId);
if (booking == null) throw new IllegalArgumentException("Booking not found: " + bookingId);
booking.cancel();
System.out.println(" >> Cancelled: " + booking);
promoteFromWaitlist(booking.getTimeSlot());
} finally {
lock.unlock();
}
}
private void promoteFromWaitlist(TimeSlot slot) {
WaitlistEntry entry = waitlistRepo.removeFirst(slot);
if (entry == null) return;
List<Table> available = getAvailableTables(slot, entry.getPartySize());
Table assigned = strategy.assignTable(available, entry.getPartySize());
if (assigned != null) {
Booking booking = new Booking("B" + (++bookingCounter), entry.getCustomerName(),
entry.getPartySize(), assigned, slot);
bookingRepo.save(booking);
System.out.println(" >> Promoted from waitlist: " + booking);
} else {
waitlistRepo.add(entry);
}
}
public void seatGuests(String bookingId) {
Booking booking = bookingRepo.findById(bookingId);
if (booking == null) throw new IllegalArgumentException("Booking not found: " + bookingId);
booking.seat();
System.out.println(" >> Seated: " + booking);
}
public void completeBooking(String bookingId) {
Booking booking = bookingRepo.findById(bookingId);
if (booking == null) throw new IllegalArgumentException("Booking not found: " + bookingId);
booking.complete();
System.out.println(" >> Completed: " + booking);
}
public List<WaitlistEntry> getWaitlist(TimeSlot slot) {
return waitlistRepo.getBySlot(slot);
}
}
# --- Booking Result ---
@dataclass
class BookingResult:
booking: Optional[Booking] = None
waitlisted: bool = False
waitlist_entry: Optional[WaitlistEntry] = None
def __str__(self):
if self.waitlisted:
return f"WAITLISTED: {self.waitlist_entry}"
return f"CONFIRMED: {self.booking}"
# --- Booking Service ---
class BookingService:
def __init__(self, strategy: TableAssignmentStrategy):
self._table_repo = InMemoryTableRepository()
self._booking_repo = InMemoryBookingRepository()
self._waitlist_repo = InMemoryWaitlistRepository()
self._strategy = strategy
self._lock = threading.Lock()
self._booking_counter = 0
self._table_counter = 0
def set_strategy(self, strategy: TableAssignmentStrategy):
self._strategy = strategy
def add_table(self, capacity: int, table_type: TableType) -> Table:
self._table_counter += 1
table = Table(f"T{self._table_counter}", capacity, table_type)
self._table_repo.save(table)
return table
def get_available_tables(self, slot: TimeSlot, party_size: int) -> List[Table]:
return [
t for t in self._table_repo.find_all()
if t.capacity >= party_size
and not self._booking_repo.find_by_table_and_overlapping_slot(t.id, slot)
]
def book_table(self, customer_name: str, party_size: int, slot: TimeSlot) -> BookingResult:
with self._lock:
available = self.get_available_tables(slot, party_size)
assigned = self._strategy.assign_table(available, party_size)
if assigned:
self._booking_counter += 1
booking = Booking(f"B{self._booking_counter}", customer_name,
party_size, assigned, slot)
self._booking_repo.save(booking)
print(f" >> Booked: {booking}")
return BookingResult(booking=booking)
else:
entry = WaitlistEntry(customer_name, party_size, slot)
self._waitlist_repo.add(entry)
print(f" >> Waitlisted: {entry}")
return BookingResult(waitlisted=True, waitlist_entry=entry)
def cancel_booking(self, booking_id: str):
with self._lock:
booking = self._booking_repo.find_by_id(booking_id)
if not booking:
raise ValueError(f"Booking not found: {booking_id}")
booking.cancel()
print(f" >> Cancelled: {booking}")
self._promote_from_waitlist(booking.time_slot)
def _promote_from_waitlist(self, slot: TimeSlot):
entry = self._waitlist_repo.remove_first(slot)
if not entry:
return
available = self.get_available_tables(slot, entry.party_size)
assigned = self._strategy.assign_table(available, entry.party_size)
if assigned:
self._booking_counter += 1
booking = Booking(f"B{self._booking_counter}", entry.customer_name,
entry.party_size, assigned, slot)
self._booking_repo.save(booking)
print(f" >> Promoted from waitlist: {booking}")
else:
self._waitlist_repo.add(entry)
def seat_guests(self, booking_id: str):
booking = self._booking_repo.find_by_id(booking_id)
if not booking:
raise ValueError(f"Booking not found: {booking_id}")
booking.seat()
print(f" >> Seated: {booking}")
def complete_booking(self, booking_id: str):
booking = self._booking_repo.find_by_id(booking_id)
if not booking:
raise ValueError(f"Booking not found: {booking_id}")
booking.complete()
print(f" >> Completed: {booking}")
def get_waitlist(self, slot: TimeSlot) -> List[WaitlistEntry]:
return self._waitlist_repo.get_by_slot(slot)
// --- Booking Result ---
struct BookingResult {
std::shared_ptr<Booking> booking;
bool waitlisted = false;
std::shared_ptr<WaitlistEntry> waitlistEntry;
std::string toString() const {
if (waitlisted) return "WAITLISTED: " + waitlistEntry->toString();
return "CONFIRMED: " + booking->toString();
}
};
// --- Booking Service ---
class BookingService {
std::vector<std::shared_ptr<Table>> tables;
std::vector<std::shared_ptr<Booking>> bookings;
std::map<TimeSlot, std::list<std::shared_ptr<WaitlistEntry>>> waitlist;
std::shared_ptr<TableAssignmentStrategy> strategy;
std::mutex mtx;
int bookingCounter = 0;
int tableCounter = 0;
public:
BookingService(std::shared_ptr<TableAssignmentStrategy> strat) : strategy(strat) {}
void setStrategy(std::shared_ptr<TableAssignmentStrategy> strat) {
strategy = strat;
}
std::shared_ptr<Table> addTable(int capacity, TableType type) {
auto table = std::make_shared<Table>();
table->id = "T" + std::to_string(++tableCounter);
table->capacity = capacity;
table->tableType = type;
tables.push_back(table);
return table;
}
std::vector<std::shared_ptr<Table>> getAvailableTables(const TimeSlot& slot, int partySize) {
std::vector<std::shared_ptr<Table>> available;
for (auto& t : tables) {
if (t->capacity < partySize) continue;
bool conflict = false;
for (auto& b : bookings) {
if (b->table->id == t->id
&& (b->status == BookingStatus::CONFIRMED || b->status == BookingStatus::SEATED)
&& b->timeSlot.overlapsWith(slot)) {
conflict = true;
break;
}
}
if (!conflict) available.push_back(t);
}
return available;
}
BookingResult bookTable(const std::string& customerName, int partySize, const TimeSlot& slot) {
std::lock_guard<std::mutex> lock(mtx);
auto available = getAvailableTables(slot, partySize);
auto assigned = strategy->assignTable(available, partySize);
if (assigned) {
auto booking = std::make_shared<Booking>();
booking->id = "B" + std::to_string(++bookingCounter);
booking->customerName = customerName;
booking->partySize = partySize;
booking->table = assigned;
booking->timeSlot = slot;
bookings.push_back(booking);
std::cout << " >> Booked: " << booking->toString() << std::endl;
return {booking, false, nullptr};
} else {
auto entry = std::make_shared<WaitlistEntry>();
entry->customerName = customerName;
entry->partySize = partySize;
entry->timeSlot = slot;
waitlist[slot].push_back(entry);
std::cout << " >> Waitlisted: " << entry->toString() << std::endl;
return {nullptr, true, entry};
}
}
void cancelBooking(const std::string& bookingId) {
std::lock_guard<std::mutex> lock(mtx);
auto booking = findBooking(bookingId);
if (!booking) throw std::runtime_error("Booking not found: " + bookingId);
booking->cancel();
std::cout << " >> Cancelled: " << booking->toString() << std::endl;
promoteFromWaitlist(booking->timeSlot);
}
void seatGuests(const std::string& bookingId) {
auto booking = findBooking(bookingId);
if (!booking) throw std::runtime_error("Booking not found: " + bookingId);
booking->seat();
std::cout << " >> Seated: " << booking->toString() << std::endl;
}
void completeBooking(const std::string& bookingId) {
auto booking = findBooking(bookingId);
if (!booking) throw std::runtime_error("Booking not found: " + bookingId);
booking->complete();
std::cout << " >> Completed: " << booking->toString() << std::endl;
}
std::list<std::shared_ptr<WaitlistEntry>> getWaitlist(const TimeSlot& slot) {
return waitlist[slot];
}
private:
std::shared_ptr<Booking> findBooking(const std::string& id) {
for (auto& b : bookings) {
if (b->id == id) return b;
}
return nullptr;
}
void promoteFromWaitlist(const TimeSlot& slot) {
auto& list = waitlist[slot];
if (list.empty()) return;
auto entry = list.front();
list.pop_front();
auto available = getAvailableTables(slot, entry->partySize);
auto assigned = strategy->assignTable(available, entry->partySize);
if (assigned) {
auto booking = std::make_shared<Booking>();
booking->id = "B" + std::to_string(++bookingCounter);
booking->customerName = entry->customerName;
booking->partySize = entry->partySize;
booking->table = assigned;
booking->timeSlot = slot;
bookings.push_back(booking);
std::cout << " >> Promoted from waitlist: " << booking->toString() << std::endl;
} else {
list.push_back(entry);
}
}
};
// --- Async Lock (single-threaded safety for async contexts) ---
class AsyncLock {
constructor() { this._queue = []; this._locked = false; }
async acquire() {
if (!this._locked) {
this._locked = true;
return;
}
return new Promise(resolve => this._queue.push(resolve));
}
release() {
if (this._queue.length > 0) {
const next = this._queue.shift();
next();
} else {
this._locked = false;
}
}
}
// --- Booking Service ---
class BookingService {
constructor(strategy) {
this.tables = [];
this.bookings = [];
this.waitlist = new Map(); // key(TimeSlot) -> WaitlistEntry[]
this.strategy = strategy;
this.lock = new AsyncLock();
this.bookingCounter = 0;
this.tableCounter = 0;
}
setStrategy(strategy) { this.strategy = strategy; }
addTable(capacity, tableType) {
const table = new Table(`T${++this.tableCounter}`, capacity, tableType);
this.tables.push(table);
return table;
}
getAvailableTables(slot, partySize) {
return this.tables.filter(t => {
if (t.capacity < partySize) return false;
const hasConflict = this.bookings.some(b =>
b.table.id === t.id &&
(b.status === BookingStatus.CONFIRMED || b.status === BookingStatus.SEATED) &&
b.timeSlot.overlapsWith(slot)
);
return !hasConflict;
});
}
async bookTable(customerName, partySize, slot) {
await this.lock.acquire();
try {
const available = this.getAvailableTables(slot, partySize);
const assigned = this.strategy.assignTable(available, partySize);
if (assigned) {
const booking = new Booking(
`B${++this.bookingCounter}`, customerName, partySize, assigned, slot
);
this.bookings.push(booking);
console.log(` >> Booked: ${booking.toString()}`);
return { booking, waitlisted: false };
} else {
const entry = new WaitlistEntry(customerName, partySize, slot);
const key = slot.key();
if (!this.waitlist.has(key)) this.waitlist.set(key, []);
this.waitlist.get(key).push(entry);
console.log(` >> Waitlisted: ${entry.toString()}`);
return { booking: null, waitlisted: true, waitlistEntry: entry };
}
} finally {
this.lock.release();
}
}
async cancelBooking(bookingId) {
await this.lock.acquire();
try {
const booking = this.bookings.find(b => b.id === bookingId);
if (!booking) throw new Error(`Booking not found: ${bookingId}`);
booking.cancel();
console.log(` >> Cancelled: ${booking.toString()}`);
this._promoteFromWaitlist(booking.timeSlot);
} finally {
this.lock.release();
}
}
_promoteFromWaitlist(slot) {
const key = slot.key();
const entries = this.waitlist.get(key);
if (!entries || entries.length === 0) return;
const entry = entries.shift();
const available = this.getAvailableTables(slot, entry.partySize);
const assigned = this.strategy.assignTable(available, entry.partySize);
if (assigned) {
const booking = new Booking(
`B${++this.bookingCounter}`, entry.customerName, entry.partySize, assigned, slot
);
this.bookings.push(booking);
console.log(` >> Promoted from waitlist: ${booking.toString()}`);
} else {
entries.unshift(entry);
}
}
seatGuests(bookingId) {
const booking = this.bookings.find(b => b.id === bookingId);
if (!booking) throw new Error(`Booking not found: ${bookingId}`);
booking.seat();
console.log(` >> Seated: ${booking.toString()}`);
}
completeBooking(bookingId) {
const booking = this.bookings.find(b => b.id === bookingId);
if (!booking) throw new Error(`Booking not found: ${bookingId}`);
booking.complete();
console.log(` >> Completed: ${booking.toString()}`);
}
getWaitlist(slot) {
return this.waitlist.get(slot.key()) || [];
}
}
Main / Demo
A complete runnable demo showing the system in action: adding tables, booking with BestFit strategy, conflict detection on overlapping slots, waitlist promotion on cancellation, state transitions, and runtime strategy swap to FirstFit.
public class RestaurantBookingSystem {
public static void main(String[] args) {
System.out.println("=== Restaurant Booking System Demo ===\n");
// Create service with BestFit strategy
BookingService service = new BookingService(new BestFitStrategy());
// Add tables
System.out.println("--- Adding Tables ---");
Table t1 = service.addTable(2, TableType.INDOOR);
Table t2 = service.addTable(4, TableType.INDOOR);
Table t3 = service.addTable(6, TableType.OUTDOOR);
Table t4 = service.addTable(8, TableType.VIP);
System.out.println("Added: " + t1 + ", " + t2 + ", " + t3 + ", " + t4);
// Define time slots
TimeSlot fridayDinner = new TimeSlot(LocalDate.of(2025, 1, 10),
LocalTime.of(19, 0), LocalTime.of(21, 0));
TimeSlot fridayLate = new TimeSlot(LocalDate.of(2025, 1, 10),
LocalTime.of(20, 0), LocalTime.of(22, 0));
// Book tables - BestFit assigns smallest fitting table
System.out.println("\n--- Booking with BestFit Strategy ---");
BookingResult r1 = service.bookTable("Alice", 2, fridayDinner);
BookingResult r2 = service.bookTable("Bob", 4, fridayDinner);
BookingResult r3 = service.bookTable("Charlie", 5, fridayDinner);
BookingResult r4 = service.bookTable("Diana", 7, fridayDinner);
// This should go to waitlist (all tables booked for overlapping slot)
System.out.println("\n--- Conflict Detection (overlapping slot) ---");
BookingResult r5 = service.bookTable("Eve", 2, fridayLate);
// Show waitlist
System.out.println("\n--- Waitlist for Friday Late ---");
service.getWaitlist(fridayLate).forEach(e -> System.out.println(" " + e));
// Cancel Alice's booking - should auto-promote Eve
System.out.println("\n--- Cancel Alice (auto-promotes waitlist) ---");
service.cancelBooking(r1.getBooking().getId());
// Demonstrate state transitions
System.out.println("\n--- State Transitions ---");
service.seatGuests(r2.getBooking().getId());
service.completeBooking(r2.getBooking().getId());
// Switch to FirstFit strategy
System.out.println("\n--- Switch to FirstFit Strategy ---");
service.setStrategy(new FirstFitStrategy());
TimeSlot saturdayDinner = new TimeSlot(LocalDate.of(2025, 1, 11),
LocalTime.of(19, 0), LocalTime.of(21, 0));
BookingResult r6 = service.bookTable("Frank", 2, saturdayDinner);
// Show available tables
System.out.println("\n--- Available tables for Saturday Dinner (party of 2) ---");
service.getAvailableTables(saturdayDinner, 2)
.forEach(t -> System.out.println(" " + t));
System.out.println("\n=== Demo Complete ===");
}
}
def main():
print("=== Restaurant Booking System Demo ===\n")
# Create service with BestFit strategy
service = BookingService(BestFitStrategy())
# Add tables
print("--- Adding Tables ---")
t1 = service.add_table(2, TableType.INDOOR)
t2 = service.add_table(4, TableType.INDOOR)
t3 = service.add_table(6, TableType.OUTDOOR)
t4 = service.add_table(8, TableType.VIP)
print(f"Added: {t1}, {t2}, {t3}, {t4}")
# Define time slots
friday_dinner = TimeSlot(date(2025, 1, 10), time(19, 0), time(21, 0))
friday_late = TimeSlot(date(2025, 1, 10), time(20, 0), time(22, 0))
# Book tables - BestFit assigns smallest fitting table
print("\n--- Booking with BestFit Strategy ---")
r1 = service.book_table("Alice", 2, friday_dinner)
r2 = service.book_table("Bob", 4, friday_dinner)
r3 = service.book_table("Charlie", 5, friday_dinner)
r4 = service.book_table("Diana", 7, friday_dinner)
# This should go to waitlist (all tables booked for overlapping slot)
print("\n--- Conflict Detection (overlapping slot) ---")
r5 = service.book_table("Eve", 2, friday_late)
# Show waitlist
print("\n--- Waitlist for Friday Late ---")
for entry in service.get_waitlist(friday_late):
print(f" {entry}")
# Cancel Alice's booking - should auto-promote Eve
print("\n--- Cancel Alice (auto-promotes waitlist) ---")
service.cancel_booking(r1.booking.id)
# Demonstrate state transitions
print("\n--- State Transitions ---")
service.seat_guests(r2.booking.id)
service.complete_booking(r2.booking.id)
# Switch to FirstFit strategy
print("\n--- Switch to FirstFit Strategy ---")
service.set_strategy(FirstFitStrategy())
saturday_dinner = TimeSlot(date(2025, 1, 11), time(19, 0), time(21, 0))
r6 = service.book_table("Frank", 2, saturday_dinner)
# Show available tables
print("\n--- Available tables for Saturday Dinner (party of 2) ---")
for t in service.get_available_tables(saturday_dinner, 2):
print(f" {t}")
print("\n=== Demo Complete ===")
if __name__ == "__main__":
main()
int main() {
std::cout << "=== Restaurant Booking System Demo ===\n" << std::endl;
auto service = std::make_unique<BookingService>(std::make_shared<BestFitStrategy>());
// Add tables
std::cout << "--- Adding Tables ---" << std::endl;
auto t1 = service->addTable(2, TableType::INDOOR);
auto t2 = service->addTable(4, TableType::INDOOR);
auto t3 = service->addTable(6, TableType::OUTDOOR);
auto t4 = service->addTable(8, TableType::VIP);
std::cout << "Added: " << t1->toString() << ", " << t2->toString()
<< ", " << t3->toString() << ", " << t4->toString() << std::endl;
// Define time slots
TimeSlot fridayDinner = {2025, 1, 10, 19, 0, 21, 0};
TimeSlot fridayLate = {2025, 1, 10, 20, 0, 22, 0};
// Book tables - BestFit assigns smallest fitting table
std::cout << "\n--- Booking with BestFit Strategy ---" << std::endl;
auto r1 = service->bookTable("Alice", 2, fridayDinner);
auto r2 = service->bookTable("Bob", 4, fridayDinner);
auto r3 = service->bookTable("Charlie", 5, fridayDinner);
auto r4 = service->bookTable("Diana", 7, fridayDinner);
// This should go to waitlist
std::cout << "\n--- Conflict Detection (overlapping slot) ---" << std::endl;
auto r5 = service->bookTable("Eve", 2, fridayLate);
// Show waitlist
std::cout << "\n--- Waitlist for Friday Late ---" << std::endl;
for (auto& e : service->getWaitlist(fridayLate)) {
std::cout << " " << e->toString() << std::endl;
}
// Cancel Alice - should auto-promote Eve
std::cout << "\n--- Cancel Alice (auto-promotes waitlist) ---" << std::endl;
service->cancelBooking(r1.booking->id);
// Demonstrate state transitions
std::cout << "\n--- State Transitions ---" << std::endl;
service->seatGuests(r2.booking->id);
service->completeBooking(r2.booking->id);
// Switch to FirstFit strategy
std::cout << "\n--- Switch to FirstFit Strategy ---" << std::endl;
service->setStrategy(std::make_shared<FirstFitStrategy>());
TimeSlot saturdayDinner = {2025, 1, 11, 19, 0, 21, 0};
auto r6 = service->bookTable("Frank", 2, saturdayDinner);
// Show available tables
std::cout << "\n--- Available tables for Saturday Dinner (party of 2) ---" << std::endl;
for (auto& t : service->getAvailableTables(saturdayDinner, 2)) {
std::cout << " " << t->toString() << std::endl;
}
std::cout << "\n=== Demo Complete ===" << std::endl;
return 0;
}
async function main() {
console.log("=== Restaurant Booking System Demo ===\n");
// Create service with BestFit strategy
const service = new BookingService(new BestFitStrategy());
// Add tables
console.log("--- Adding Tables ---");
const t1 = service.addTable(2, TableType.INDOOR);
const t2 = service.addTable(4, TableType.INDOOR);
const t3 = service.addTable(6, TableType.OUTDOOR);
const t4 = service.addTable(8, TableType.VIP);
console.log(`Added: ${t1}, ${t2}, ${t3}, ${t4}`);
// Define time slots
const fridayDinner = new TimeSlot("2025-01-10", "19:00", "21:00");
const fridayLate = new TimeSlot("2025-01-10", "20:00", "22:00");
// Book tables - BestFit assigns smallest fitting table
console.log("\n--- Booking with BestFit Strategy ---");
const r1 = await service.bookTable("Alice", 2, fridayDinner);
const r2 = await service.bookTable("Bob", 4, fridayDinner);
const r3 = await service.bookTable("Charlie", 5, fridayDinner);
const r4 = await service.bookTable("Diana", 7, fridayDinner);
// This should go to waitlist (all tables booked for overlapping slot)
console.log("\n--- Conflict Detection (overlapping slot) ---");
const r5 = await service.bookTable("Eve", 2, fridayLate);
// Show waitlist
console.log("\n--- Waitlist for Friday Late ---");
service.getWaitlist(fridayLate).forEach(e => console.log(` ${e.toString()}`));
// Cancel Alice's booking - should auto-promote Eve
console.log("\n--- Cancel Alice (auto-promotes waitlist) ---");
await service.cancelBooking(r1.booking.id);
// Demonstrate state transitions
console.log("\n--- State Transitions ---");
service.seatGuests(r2.booking.id);
service.completeBooking(r2.booking.id);
// Switch to FirstFit strategy
console.log("\n--- Switch to FirstFit Strategy ---");
service.setStrategy(new FirstFitStrategy());
const saturdayDinner = new TimeSlot("2025-01-11", "19:00", "21:00");
const r6 = await service.bookTable("Frank", 2, saturdayDinner);
// Show available tables
console.log("\n--- Available tables for Saturday Dinner (party of 2) ---");
service.getAvailableTables(saturdayDinner, 2).forEach(t => console.log(` ${t.toString()}`));
console.log("\n=== Demo Complete ===");
}
main();
Follow-up Questions
-
How would you handle recurring reservations (e.g., every Friday at 7pm)? Consider a RecurringBooking entity that generates individual Booking instances for each occurrence. Handle conflicts on specific dates and allow individual cancellations without affecting the series.
-
How would you implement a no-show policy with automatic cancellation after 15 minutes? A scheduled task checks CONFIRMED bookings past their start time + grace period. Transition to CANCELLED and trigger waitlist promotion. Consider SMS/push notifications before auto-cancel.
-
How would you scale this for a chain with 100+ restaurants? Shard by restaurant ID. Each restaurantโs bookings are independent, so horizontal partitioning is natural. Use a service registry for routing and consider eventual consistency for cross-restaurant analytics.
-
How would you add dynamic pricing (premium slots cost more)? Introduce a PricingStrategy interface. Factor in time slot popularity, table type (VIP premium), party size, and day of week. Store pricing rules as configurable metadata per restaurant.
-
How would you handle table merging for large parties? Add a CompositeTable concept โ multiple adjacent tables can be merged. Track which tables are mergeable (adjacency metadata). The assignment strategy must consider both single tables and valid merge combinations.
-
How would you integrate with a payment system for deposits? Add a PaymentService dependency. On booking confirmation, initiate a hold/charge. On cancellation within policy window, trigger refund. Use saga pattern for the book-then-pay flow to handle partial failures.
-
How would you implement real-time availability updates for a web UI? Use WebSockets or Server-Sent Events (SSE). On every booking/cancellation, publish an event to a pub/sub channel per restaurant. Connected clients receive instant updates to refresh the availability grid.
Related Designs
- Inventory Management โ Similar repository pattern and thread-safe stock operations
- Order Management โ Comparable state machine for order lifecycle
Related Concepts
Scale this design past a single process and these are the concepts it runs into:
- Distributed Locking โ โ the booking lock has to become a distributed lock once more than one node takes reservations
- Transactions & Isolation Levels โ โ the overlap check and the insert must be one isolated unit or two parties get the same table
- Idempotency โ โ a retried reservation must not create a second booking
- Message Queues โ โ waitlist promotion and reminder notices belong on a queue rather than in the cancellation path
Discussion
Newest first