Facebook Pixel

Worked Example: Restaurant Table Reservation

This article walks the six-step delivery framework end-to-end on a single problem: a restaurant table-reservation system. The archetype is data-management — there are resources (tables), actors (guests and staff), and transactions (reservations) linking them, with business rules governing availability. The steps are worked in order to show how each one constrains the next.

A note on vocabulary: this example uses terms such as value object and mentions UML class diagrams in passing. Both are covered properly later in the course — value objects in the class-design module and UML notation in Module 2. You are not expected to know them yet; treat them as previews.

Step 1: Requirements

The prompt is: "Design a system that lets a restaurant manage table reservations."

Before writing anything, clarify the scope. Good questions to ask out loud:

"Are we building the guest-facing booking interface, or the staff back-office, or both?" "Do guests book a specific table, or a party size and the system assigns a table?" "Do we handle payments and deposits, or is that out of scope?" "Can a reservation be cancelled or modified after confirmation?" "Is there a walk-in flow, or only advance reservations?"

Assume the interviewer says: guests request a reservation by party size, date, and time; the system assigns a suitable table automatically; cancellation is supported; payments are out of scope; walk-ins are out of scope for now.

In scope: create reservation (party size, date, time slot), assign table automatically, cancel reservation, check table availability.

Out of scope: payments, walk-ins, table layout / floor plan, staff scheduling.

Step 2: Entities and Relationships

Pull the nouns from the requirements: Guest, Table, TimeSlot, Reservation, and Restaurant as the aggregate root. Sketch the ownership before writing any fields.

A Reservation links one Guest to one Table for a specific TimeSlot. A Table has a fixed capacity and belongs to the Restaurant. The Restaurant owns all tables and all reservations, and it is the entry point for search and booking operations.

Step 3: Class Design

Work through each entity in turn, deriving state and behavior directly from the requirements.

A Table is a pure value object: it holds only table_id and capacity. There is no global available/reserved flag on the table itself — availability is a per-slot fact derived from the reservation list, not a property of the table.

A TimeSlot wraps a date and an hour so that two reservations at the same table but different hours are unambiguously comparable. Making TimeSlot a value object with a well-defined equality check eliminates an entire class of double-booking bugs.

A Reservation ties a Guest, a Table, and a TimeSlot together. It carries its own status (CONFIRMED, CANCELLED) and exposes cancel(), which marks only that reservation cancelled. No other reservation is affected.

A Guest holds contact information used in confirmation messages and for lookup.

The Restaurant owns the search and booking logic. find_available_table(party_size, slot) collects the IDs of tables that have a CONFIRMED reservation at the requested slot, then returns the smallest table whose ID is not in that set and whose capacity fits the party. book() creates the Reservation and appends it to the restaurant's list — no flag on Table is touched. These two methods are the core of the system; the rest is supporting infrastructure.

Step 4: Implementation

The implementation below covers the happy path — successful booking and cancellation — with the core guards in place. Edge cases (no table available, already-cancelled reservation) raise explicit errors rather than returning None, which forces callers to handle failure explicitly.

1from __future__ import annotations
2from dataclasses import dataclass
3from datetime import date
4from enum import Enum, auto
5
6
7class ReservationStatus(Enum):
8    CONFIRMED = auto()
9    CANCELLED = auto()
10
11
12@dataclass(frozen=True)
13class TimeSlot:
14    date: date
15    hour: int  # 0-23
16
17    def __str__(self) -> str:
18        return f"{self.date} {self.hour:02d}:00"
19
20
21@dataclass
22class Guest:
23    name: str
24    email: str
25
26
27@dataclass
28class Table:
29    table_id: str
30    capacity: int
31
32
33class Reservation:
34    def __init__(
35        self,
36        reservation_id: str,
37        guest: Guest,
38        table: Table,
39        slot: TimeSlot,
40        party_size: int,
41    ) -> None:
42        self.reservation_id = reservation_id
43        self.guest = guest
44        self.table = table
45        self.slot = slot
46        self.party_size = party_size
47        self._status = ReservationStatus.CONFIRMED
48
49    @property
50    def status(self) -> ReservationStatus:
51        return self._status
52
53    def cancel(self) -> None:
54        if self._status is ReservationStatus.CANCELLED:
55            raise ValueError("Reservation is already cancelled")
56        self._status = ReservationStatus.CANCELLED
57
58
59class Restaurant:
60    def __init__(self, name: str) -> None:
61        self.name = name
62        self._tables: list[Table] = []
63        self._reservations: list[Reservation] = []
64        self._next_id = 1
65
66    def add_table(self, table: Table) -> None:
67        self._tables.append(table)
68
69    def find_available_table(
70        self, party_size: int, slot: TimeSlot
71    ) -> Table | None:
72        # Check which tables are already booked at this slot
73        booked_ids = {
74            r.table.table_id
75            for r in self._reservations
76            if r.slot == slot and r.status is ReservationStatus.CONFIRMED
77        }
78        candidates = [
79            t for t in self._tables
80            if t.table_id not in booked_ids and t.capacity >= party_size
81        ]
82        # Assign the smallest table that fits the party
83        return min(candidates, key=lambda t: t.capacity, default=None)
84
85    def book(
86        self, guest: Guest, party_size: int, slot: TimeSlot
87    ) -> Reservation:
88        table = self.find_available_table(party_size, slot)
89        if table is None:
90            raise ValueError(
91                f"No table available for {party_size} guests at {slot}"
92            )
93        reservation_id = f"R{self._next_id:04d}"
94        self._next_id += 1
95        reservation = Reservation(reservation_id, guest, table, slot, party_size)
96        self._reservations.append(reservation)
97        return reservation

Step 5: Verification

Trace a concrete booking scenario tick by tick using the objects above.


The restaurant opens with three tables: T01 (2-seat), T02 (4-seat), T03 (6-seat). A guest, Maya Chen, requests a table for 3 people on the evening of July 4 at 19:00.

Restaurant.book(maya, 3, TimeSlot("2025-07-04", 19)) calls find_available_table(3, slot). No reservations exist yet, so booked_ids is empty. Candidates are T02 (4-seat) and T03 (6-seat). The smallest fit is T02.

A Reservation (R0001) is created linking Maya to T02 at that slot and appended to the restaurant's list. R0001 is returned with status CONFIRMED. No flag on T02 is changed.

An hour later, a second guest requests 4 seats at the same slot. find_available_table(4, slot) now sees T02 in booked_ids (R0001 is CONFIRMED at that slot). Candidates reduce to T03 only. T03 is assigned and R0002 is created.

Now consider a third guest who wants 3 seats at 20:00 — a different slot. booked_ids for that slot is empty (R0001 and R0002 are at 19:00), so T02 is freely available at 20:00 and gets assigned as the smallest fit. The same physical table serves two different slots without conflict.

Maya cancels: R0001.cancel() sets R0001._status to CANCELLED. Nothing else changes. If a fourth guest now requests 2 seats at the 19:00 slot, find_available_table sees only R0002 (CONFIRMED at 19:00) in booked_ids, so T01 and T02 are both candidates; T01 (smallest fit) is assigned.

The trace confirms there are no double-bookings, that cancellation affects only its own reservation, that the same table can serve multiple slots, and that smallest-fit assignment works correctly.

Step 6: Extensibility — Waitlist Follow-up

The interviewer asks: "What if we want to add a waitlist so guests can queue for a slot that is currently full?"

The current design absorbs this without touching Table, Reservation, or TimeSlot. The change is localized to Restaurant:

A Waitlist is a mapping from TimeSlot to an ordered list of (Guest, party_size) entries. Restaurant.book() already raises when no table is available. We intercept that case and add the guest to the waitlist instead of raising. When any reservation is cancelled — Reservation.cancel() already marks the reservation CANCELLED — the restaurant can observe that event (via a callback or a post-cancel hook) and automatically promote the first waitlist entry that fits a now-free slot for the cancelled reservation's table and time.

The key observation is that cancel() already does the right thing structurally: it marks the reservation invalid, which makes find_available_table treat the slot as free again. Adding a waitlist is a Restaurant-level concern that re-calls book() for the next waiting guest after a cancellation. No existing class needs a new field or a changed signature.

The waitlist is a follow-up requirement that touches only one class and adds behavior at a clean seam, rather than cutting open every class in the design. That outcome is what the framework's extensibility step is intended to demonstrate.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro