Restaurant Management
Table allocation, order lifecycle, and kitchen queue coordination. Strategy picks tables, State drives orders through their lifecycle, and Observer wires the kitchen to the wait staff.
Key Abstractions
Facade. Entry point for reservations, ordering, and billing.
Physical seating unit with a capacity and occupancy status.
Binds a party to a future time slot and table assignment.
Collection of menu items moving through PLACED/PREPARING/READY/SERVED states.
Name, price, category. Immutable value object.
Processes order queue. Notifies observers when an order is ready.
Aggregates order totals, applies tax and optional tip.
Class Diagram
Why This Problem Matters
Restaurant management sits at the intersection of three classic design challenges: resource allocation (tables), state machines (orders), and event-driven communication (kitchen to wait staff). Most candidates jump straight into coding a Restaurant class with a hundred methods. That misses the point.
The real problem is decomposition. A restaurant has physical resources (tables), workflows (order lifecycle), and coordination (kitchen notifies waiter). Each of those deserves its own abstraction with clear boundaries.
Requirements
Functional
- Manage tables with different seating capacities
- Accept reservations for a party size and time slot
- Place orders with menu items against an occupied table
- Track order lifecycle: PLACED, PREPARING, READY, SERVED
- Kitchen processes orders in FIFO sequence and notifies staff when ready
- Generate bills with subtotal, tax, and optional tip
Non-Functional
- Thread-safe table allocation for concurrent reservation requests
- Pluggable table assignment strategy without modifying core logic
- Order state transitions enforced at the domain level, not by callers
Design Decisions
Why Strategy for table allocation?
A fast-food joint wants first-fit: seat people fast, don't optimize. A fine-dining restaurant wants best-fit: put a couple at the 2-top, not the 8-top. Making this a strategy means the allocation logic changes by swapping one object. Restaurant code stays untouched. During an interview, this is the kind of decision that shows you think about extensibility without over-engineering.
Why State pattern for order lifecycle?
Orders move through a strict sequence: PLACED, PREPARING, READY, SERVED. You never go from PLACED to SERVED directly. Encoding transitions in the OrderStatus enum (or a transition map) means invalid state changes blow up immediately instead of silently corrupting data. Compare this to a string field where anyone can write "ready" or "Rdy" or "REDDY" and nothing complains.
Why a separate Kitchen class?
Kitchen owns the preparation queue and fires events when food is done. Without this separation, Restaurant becomes a God class that manages tables AND orders AND cooking AND notifications. Kitchen also gives you a natural place to add capacity limits, priority ordering, or parallel preparation later.
Why Observer for kitchen notifications?
When an order is ready, the waiter needs to know. So does the customer's app. So does the manager's dashboard. Observer lets you attach all of these without the Kitchen knowing about any of them. Adding a new notification channel is one new class, zero changes to Kitchen.
Interview Follow-ups
- "How would you handle split bills?" Add a
BillSplitterthat takes a Bill and a split strategy (equal, by-item, custom amounts). Bill stays immutable. - "What about waitlist when all tables are full?" Add a
WaitlistQueuewith party size and estimated wait. When a table frees up, notify the next fitting party via Observer. - "How would you add menu item modifiers (no onions, extra cheese)?" Decorator pattern on MenuItem, or a simpler approach: add a
modifiers: list[str]field to an OrderLineItem wrapper. - "What about multiple kitchens (bar, hot kitchen, cold kitchen)?" Route order items to the appropriate kitchen based on MenuItem category. Each kitchen is an independent Kitchen instance with its own queue.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3 from enum import Enum
4 from dataclasses import dataclass, field
5 from datetime import datetime
6 from threading import Lock
7 from typing import Protocol
8 from collections import deque
9 import uuid
10
11
12 class TableStatus(Enum):
13 AVAILABLE = "available"
14 OCCUPIED = "occupied"
15 RESERVED = "reserved"
16
17 class OrderStatus(Enum):
18 PLACED = "placed"
19 PREPARING = "preparing"
20 READY = "ready"
21 SERVED = "served"
22
23 _STATUS_TRANSITIONS: dict[OrderStatus, OrderStatus] = {
24 OrderStatus.PLACED: OrderStatus.PREPARING,
25 OrderStatus.PREPARING: OrderStatus.READY,
26 OrderStatus.READY: OrderStatus.SERVED,
27 }
28
29
30 @dataclass(frozen=True)
31 class MenuItem:
32 name: str
33 price: float
34 category: str
35
36
37 class Order:
38 def __init__(self, order_id: str, items: list[MenuItem]):
39 self.id = order_id
40 self.items = list(items)
41 self.status = OrderStatus.PLACED
42
43 def add_item(self, item: MenuItem) -> None:
44 if self.status != OrderStatus.PLACED:
45 raise RuntimeError("Cannot modify order after it leaves PLACED state")
46 self.items.append(item)
47
48 def advance(self) -> None:
49 nxt = _STATUS_TRANSITIONS.get(self.status)
50 if nxt is None:
51 raise RuntimeError(f"Order {self.id} is already SERVED, cannot advance")
52 self.status = nxt
53
54 def total(self) -> float:
55 return sum(item.price for item in self.items)
56
57 def __repr__(self) -> str:
58 return f"Order({self.id}, {self.status.value}, ${self.total():.2f})"
59
60
61 class Table:
62 def __init__(self, table_id: str, capacity: int):
63 self.table_id = table_id
64 self.capacity = capacity
65 self.status = TableStatus.AVAILABLE
66 self.current_order: Order | None = None
67
68 def assign(self) -> None:
69 if self.status != TableStatus.AVAILABLE:
70 raise RuntimeError(f"Table {self.table_id} is {self.status.value}")
71 self.status = TableStatus.OCCUPIED
72
73 def release(self) -> None:
74 self.status = TableStatus.AVAILABLE
75 self.current_order = None
76
77 def __repr__(self) -> str:
78 return f"Table({self.table_id}, seats={self.capacity}, {self.status.value})"
79
80
81 @dataclass
82 class Reservation:
83 id: str
84 guest_name: str
85 party_size: int
86 time_slot: datetime
87 table: Table
88
89
90 class KitchenObserver(Protocol):
91 def on_order_ready(self, order: Order) -> None: ...
92
93
94 class WaiterNotifier:
95 def on_order_ready(self, order: Order) -> None:
96 print(f" [Waiter] Order {order.id} is ready for pickup!")
97
98
99 class Kitchen:
100 def __init__(self) -> None:
101 self._queue: deque[Order] = deque()
102 self._observers: list[KitchenObserver] = []
103
104 def add_observer(self, observer: KitchenObserver) -> None:
105 self._observers.append(observer)
106
107 def submit(self, order: Order) -> None:
108 order.advance() # PLACED -> PREPARING
109 self._queue.append(order)
110
111 def prepare_next(self) -> Order | None:
112 if not self._queue:
113 return None
114 order = self._queue.popleft()
115 order.advance() # PREPARING -> READY
116 for obs in self._observers:
117 obs.on_order_ready(order)
118 return order
119
120
121 class TableAllocationStrategy(Protocol):
122 def allocate(self, tables: list[Table], party_size: int) -> Table | None: ...
123
124 class FirstFitStrategy:
125 """Grabs the first table that fits."""
126 def allocate(self, tables: list[Table], party_size: int) -> Table | None:
127 for t in tables:
128 if t.status == TableStatus.AVAILABLE and t.capacity >= party_size:
129 return t
130 return None
131
132 class BestFitStrategy:
133 """Picks the smallest adequate table to minimize wasted seats."""
134 def allocate(self, tables: list[Table], party_size: int) -> Table | None:
135 candidates = [
136 t for t in tables
137 if t.status == TableStatus.AVAILABLE and t.capacity >= party_size
138 ]
139 if not candidates:
140 return None
141 return min(candidates, key=lambda t: t.capacity)
142
143
144 @dataclass
145 class Bill:
146 order: Order
147 tax_rate: float = 0.08
148 tip: float = 0.0
149
150 def subtotal(self) -> float:
151 return self.order.total()
152
153 def tax(self) -> float:
154 return self.subtotal() * self.tax_rate
155
156 def total(self) -> float:
157 return self.subtotal() + self.tax() + self.tip
158
159
160 class Restaurant:
161 def __init__(self, tables: list[Table], strategy: TableAllocationStrategy | None = None):
162 self._tables = tables
163 self._strategy = strategy or BestFitStrategy()
164 self._kitchen = Kitchen()
165 self._kitchen.add_observer(WaiterNotifier())
166 self._reservations: list[Reservation] = []
167 self._lock = Lock()
168
169 def make_reservation(self, guest: str, party_size: int, time_slot: datetime) -> Reservation:
170 with self._lock:
171 table = self._strategy.allocate(self._tables, party_size)
172 if table is None:
173 raise RuntimeError(f"No table available for party of {party_size}")
174 table.assign()
175 res = Reservation(
176 id=str(uuid.uuid4())[:8],
177 guest_name=guest,
178 party_size=party_size,
179 time_slot=time_slot,
180 table=table,
181 )
182 self._reservations.append(res)
183 return res
184
185 def place_order(self, table_id: str, items: list[MenuItem]) -> Order:
186 table = self._find_table(table_id)
187 if table.status != TableStatus.OCCUPIED:
188 raise RuntimeError(f"Table {table_id} is not occupied")
189 order = Order(order_id=str(uuid.uuid4())[:8], items=items)
190 table.current_order = order
191 self._kitchen.submit(order)
192 return order
193
194 def cook_next(self) -> Order | None:
195 return self._kitchen.prepare_next()
196
197 def generate_bill(self, table_id: str, tip: float = 0.0) -> Bill:
198 table = self._find_table(table_id)
199 if table.current_order is None:
200 raise RuntimeError(f"No order on table {table_id}")
201 bill = Bill(order=table.current_order, tip=tip)
202 table.release()
203 return bill
204
205 def _find_table(self, table_id: str) -> Table:
206 for t in self._tables:
207 if t.table_id == table_id:
208 return t
209 raise ValueError(f"Unknown table: {table_id}")
210
211 def availability(self) -> dict[str, str]:
212 return {t.table_id: t.status.value for t in self._tables}
213
214
215 if __name__ == "__main__":
216 menu = [
217 MenuItem("Margherita Pizza", 12.99, "Main"),
218 MenuItem("Caesar Salad", 8.49, "Starter"),
219 MenuItem("Tiramisu", 7.99, "Dessert"),
220 ]
221
222 tables = [
223 Table("T1", capacity=2),
224 Table("T2", capacity=4),
225 Table("T3", capacity=4),
226 Table("T4", capacity=6),
227 Table("T5", capacity=8),
228 ]
229 restaurant = Restaurant(tables, BestFitStrategy())
230
231 print("=== Restaurant Management System ===\n")
232
233 res = restaurant.make_reservation("Alice", party_size=3, time_slot=datetime.now())
234 print(f"Reservation: {res.guest_name}, party of {res.party_size} at {res.table.table_id}")
235 print(f"Availability: {restaurant.availability()}\n")
236
237 order = restaurant.place_order(res.table.table_id, [menu[0], menu[1], menu[2]])
238 print(f"Order placed: {order}\n")
239
240 print("Kitchen prepares next order:")
241 ready_order = restaurant.cook_next()
242 print(f" Order status after cooking: {ready_order.status.value}\n")
243
244 ready_order.advance() # READY -> SERVED
245 print(f"Order served. Status: {ready_order.status.value}")
246
247 bill = restaurant.generate_bill(res.table.table_id, tip=5.00)
248 print(f"\nBill for {res.table.table_id}:")
249 print(f" Subtotal: ${bill.subtotal():.2f}")
250 print(f" Tax: ${bill.tax():.2f}")
251 print(f" Tip: ${bill.tip:.2f}")
252 print(f" Total: ${bill.total():.2f}")
253 print(f"\nFinal availability: {restaurant.availability()}")Common Mistakes
- ✗Letting Restaurant manage individual order state transitions. That's the Order's job. Restaurant should delegate.
- ✗Skipping the Kitchen abstraction and having Order prepare itself. That conflates what you ordered with how it gets made.
- ✗Using strings for order status instead of an enum. One typo ('PREPRING') and your state machine silently breaks.
- ✗Forgetting thread safety on table assignment. Two hosts seating parties simultaneously can double-book the same table.
Key Points
- ✓Strategy pattern for table allocation: first-fit grabs the first table that fits the party, best-fit picks the smallest adequate table to minimize wasted seats
- ✓State pattern on Order so each status transition is explicit. PLACED can move to PREPARING, PREPARING to READY, READY to SERVED. No random jumps.
- ✓Observer decouples Kitchen from wait staff. Kitchen fires an event when food is ready. The waiter listener handles delivery. Adding a notification to the customer's phone is just another observer.
- ✓Enums for OrderStatus and TableStatus. No magic strings drifting through the codebase.