Restaurant Management System

Tables, reservations, orders, menu, bill, waiter, chef. Order state machine plus table-assignment Strategy.

System Intermediate
17 min read
ood case-study restaurant state-pattern strategy-pattern

Context#

A restaurant management system runs a single dining venue: guests arrive (with or without a reservation), a host assigns them to a table, a waiter takes the order, the kitchen prepares it, the waiter delivers it, the guests pay, and the table is reset. Underneath sit a menu (categorised items with prices), a reservation calendar, and staff with distinct roles (host, waiter, chef). The prompt is mid-density — small enough to finish in 45 minutes at an Intermediate level, structured enough to surface the State and Strategy patterns without straining.

The interviewer’s hidden objectives, in roughly the order they will be tested:

  • Can you separate the entities cleanly — Table is physical, Reservation is a future claim on a table, Order is the consumption event, Bill is the settlement — and notice they are siblings, not parents and children?
  • Can you model the order state machine correctly: Placed → Preparing → Ready → Served → Paid with Cancelled as a side-branch reachable from the early states only?
  • Can you justify a table-assignment Strategy (greedy nearest-fit / minimise-walk / partition-friendly) instead of a single hardcoded algorithm?
  • Can you model the bill-splitting Strategy (equal / itemised / percentage) so the room sees you reach for the pattern when a domain has multiple legitimate policies?
  • Can you scope out kitchen routing, inventory, payroll, supplier orders, and POS hardware as out-of-scope — naming each so the interviewer cannot ambush with “but how does the kitchen prioritise tickets?”

Requirements (functional and non-functional)#

The scope below is what a 45-minute Intermediate round expects.

Functional — in scope.

  • A restaurant has a fixed set of Tables with capacities (2, 4, 6, 8).
  • Guests may make a Reservation for a party size at a future time, or walk in.
  • The host assigns an arriving party to a free table of sufficient capacity using a configurable table-assignment strategy.
  • A waiter takes an Order for the party, composed of OrderItems chosen from the Menu.
  • The kitchen prepares each order item; the order transitions Placed → Preparing → Ready → Served.
  • On request, the waiter generates a Bill for the order; the party pays. Bill splitting is configurable (equal / itemised / percentage).
  • After payment the table is reset and becomes available again.
  • A party may cancel their order before it reaches Ready; after that, cancellation is disallowed.

Functional — out of scope (called out explicitly).

  • Kitchen routing and ticket prioritisation — which chef takes which ticket, parallel station orchestration. Mention Kitchen as a façade and move on.
  • Inventory and ingredient depletion. A menu item is “available” or “out today”; deeper tracking is out of scope.
  • Payroll, scheduling, supplier orders, accounting integration.
  • Reservation overbooking, no-show fees, deposit handling. Mention as a follow-up extension.
  • Multi-venue / chain / franchise. This is a single venue.
  • Online ordering / delivery. This is dine-in only; the design extends, but the round is dine-in.

Non-functional.

  • A single venue has on the order of 100 tables, 10–20 staff, a menu of 200 items. In-process data structures are sufficient.
  • Table-assignment latency under 100 ms for a party arrival.
  • Order-state transitions must be atomic per order; two waiters cannot simultaneously mark the same order Served and double-bill the party.
  • Concurrency: multiple waiters interact with multiple tables; the design must avoid double-assigning a table or double-billing an order.

Use case diagram#

┌─────────────────┐ ┌─────────────────┐
│ Guest │ │ Host │
└────────┬────────┘ └────────┬────────┘
│ │
┌───────────────┼──────────────┐ ┌──────────────┼───────────────┐
▼ ▼ ▼ ▼ ▼ ▼
[reserve] [walk-in] [pay bill] [assign table] [cancel res] [reset table]
│ │ │ │ │ │
└───────────────┴────┬─────────┴──────────────┴──────────────┴───────────────┘
┌────────────────────────────┐
│ Restaurant System │
└─────────┬──────────────────┘
┌────────────────┼────────────────┐
│ │ │
┌────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Waiter │ │ Chef │ │ Cashier │
└──────────┘ └───────────┘ └───────────┘
take order prepare settle payment
serve order mark ready
generate bill

Five primary actors. Cashier is a role; in small venues the waiter does this. The system is a single venue.

Class diagram#

┌──────────────────────────┐
│ Restaurant │
├──────────────────────────┤
│ tables : List<Table> │
│ menu : Menu │
│ staff : List<Staff> │
│ assignment : TableAssignmentStrategy │
└─────────┬────────────────┘
│ 1..*
┌──────────────────────────┐
│ Table │
├──────────────────────────┤
│ id, capacity │
│ status : TableStatus │ // FREE, OCCUPIED, RESERVED, DIRTY
│ currentOrder : Order? │
└──────────────────────────┘
┌──────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ Reservation │ │ Order │ │ Bill │
├──────────────────────┤ ├──────────────────────────┤ ├──────────────────────────┤
│ id, guestName │ │ id, table, waiter │ │ id, order │
│ partySize, atTime │ │ items : List<OrderItem> │ │ subtotal, tax, tip │
│ table? │ │ status : OrderState │ │ split : BillSplitStrategy│
│ status : ResState │ │ placedAt, servedAt │ │ total : Money │
└──────────────────────┘ └──────────────────────────┘ └──────────────────────────┘
┌──────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ Menu │ │ MenuItem │ │ OrderItem │
├──────────────────────┤ ├──────────────────────────┤ ├──────────────────────────┤
│ items : List<MenuItem>│ │ id, name, price │ │ menuItem, quantity │
│ findById(id) │ │ category, available │ │ notes (e.g. "no onions") │
└──────────────────────┘ └──────────────────────────┘ └──────────────────────────┘
┌──────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ Staff │ │ TableAssignmentStrategy│◁── │ BillSplitStrategy │◁── EqualSplit,
├──────────────────────┤ ├──────────────────────────┤ ├──────────────────────────┤ ItemisedSplit,
│ id, name, role │ │ assign(party, tables) │ │ split(bill, payers) │ PercentageSplit
└──────┬───────────────┘ │ : Optional<Table> │ │ : Map<Payer, Money> │
│ └──────────────────────────┘ └──────────────────────────┘
┌──────┴───────┐
▼ ▼
Waiter Chef // Host is a role on a generic Staff

Three patterns are doing the load-bearing work:

  • State on Order.statusPlaced → Preparing → Ready → Served → Paid, with Cancelled reachable only from Placed or Preparing. The state machine is the safety net against double-billing, premature cancellation, and out-of-order serving.
  • Strategy on TableAssignmentStrategy. Common policies: NearestFitAssignment (smallest table that fits the party, simple and the default), PartitionFriendlyAssignment (keep large tables free for larger parties), MinimiseWalkAssignment (cluster a waiter’s assigned tables together). The host class does not switch on policy.
  • Strategy on BillSplitStrategy. EqualSplit (divide total by N), ItemisedSplit (each payer pays for their own items, taxes pro-rated), PercentageSplit (per-payer percentages sum to 100). Same shape — different policy.

What is not in the diagram and that is deliberate:

  • Kitchen is a façade, not a richly modelled aggregate. It receives a ticket per ordered item and emits Ready events. Routing among chefs, station orchestration, and ticket prioritisation are out of scope; the façade is the seam.
  • Reservation is a separate aggregate from Order. A reservation is a future claim on a table; an order is the consumption event. They never merge — a reservation either materialises into a Table.status = RESERVED lock or expires.
  • No god RestaurantManager class. Assignment is on the host (via the strategy); the kitchen is its own façade; billing is on the cashier/waiter (via the bill strategy). The Restaurant aggregate holds references, it does not own all behaviour.
  • OrderItem is its own class, not just (MenuItem, qty). It carries notes (“no onions”), per-item state (some items can be ready while others are still preparing), and the per-item price snapshot for the bill. Inlining as a tuple breaks the moment the menu price changes mid-evening.

Sequence diagram (key flows)#

The arrival-to-seating flow:

Guest Host Restaurant TableAssignmentStrategy Table
│ arrive(party=4) │ │ │ │
│─────────────────►│ │ │ │
│ │ seat(party)│ │ │
│ │───────────►│ │ │
│ │ │ assign(party, tables) │
│ │ │─────────────────►│ │
│ │ │ Table t │
│ │ │◄─────────────────│ │
│ │ │ t.status:=OCCUPIED │
│ │ │─────────────────────────────────────────►│
│ │ table t │ │ │
│ │◄───────────│ │ │
│ "table 7" │ │ │ │
│◄─────────────────│ │ │ │

The order-to-payment flow:

Guest Waiter Order Kitchen Table Bill BillSplitStrategy
│ order(items) │ │ │ │ │ │
│─────────────►│ │ │ │ │ │
│ │ place(items) │ │ │ │
│ │───────────►│ │ │ │ │
│ │ │ status:=PLACED │ │ │
│ │ │ submit(items) │ │ │
│ │ │───────────►│ │ │ │
│ │ │ │ prepare … │ │ │
│ │ │ │── ready ────►│ │ │
│ │ │ status:=READY │ │ │
│ │ notify ready │ │ │
│ │◄───────────│ │ │ │ │
│ served │ │ │ │ │ │
│◄─────────────│ status:=SERVED │ │ │ │
│ │ │ │ │ │ │
│ "bill, please" │ │ │ │ │
│─────────────►│ │ │ │ │ │
│ │ generate(order) │ │ │ │
│ │─────────────────────────────────────────────────────►│ │
│ │ │ │ │ │ split(payers)│
│ │ │ │ │ │─────────────►│
│ │ │ │ │ │ per-payer amounts
│ │ │ │ │ │◄─────────────│
│ │ itemised bill │ │
│ │◄─────────────────────────────────────────────────────│ │
│ pay │ │ │ │ │ │
│─────────────►│ │ │ │ │ │
│ │ status:=PAID │ │ │ │
│ │ table.reset() │ │ │ │
│ │────────────────────────────────────────►│ │ │

Activity diagram (for non-trivial state)#

The Order state machine is where the safety rules live:

┌─────────┐
│ start │
└────┬────┘
┌──────────┐
│ PLACED │ ──── kitchen accepts ────► ┌────────────┐
└────┬─────┘ │ PREPARING │
│ └────┬───────┘
│ cancel() (allowed) │
│ │ cancel() (allowed)
▼ ▼
┌────────────┐ ┌────────────┐
│ CANCELLED │◄───────────────────────│ CANCELLED │
└────────────┘ └────────────┘
┌────────────┐
(all items ready) ────────►│ READY │
└────┬───────┘
▼ serve()
┌────────────┐
│ SERVED │
└────┬───────┘
▼ pay()
┌────────────┐
│ PAID │ (terminal)
└────────────┘

The order machine carries three invariants:

  • cancel() is allowed only from PLACED or PREPARING. Once the kitchen has plated an item (READY), waste is realised and the order must complete.
  • READY → SERVED requires waiter action; an auto-transition would let the order skip the served check and bill the table while plates sit at the pass.
  • SERVED → PAID is the only path that releases the table (via Table.reset()). Cancelled is terminal but does not release the table because the party is still seated; release happens when the party leaves.

The Table.status state machine is the smaller partner:

FREE ──► RESERVED ──► OCCUPIED ──► DIRTY ──► FREE
(booking) (party seated) (party leaves) (cleaned)

RESERVED → OCCUPIED only when the reserving party arrives. A no-show after the grace window transitions the table back to FREE.

Java implementation#

A representative slice. The order lifecycle, the table-assignment strategy, and the bill split are the load-bearing pieces.

public enum TableStatus { FREE, RESERVED, OCCUPIED, DIRTY }
public enum OrderState { PLACED, PREPARING, READY, SERVED, PAID, CANCELLED }
public enum Role { HOST, WAITER, CHEF, CASHIER }
public final class Table {
private final int id;
private final int capacity;
private TableStatus status = TableStatus.FREE;
private Order currentOrder;
public Table(int id, int capacity) { this.id = id; this.capacity = capacity; }
public synchronized boolean seat(Order o) {
if (status != TableStatus.FREE && status != TableStatus.RESERVED) return false;
this.status = TableStatus.OCCUPIED;
this.currentOrder = o;
return true;
}
public synchronized void reset() {
this.status = TableStatus.DIRTY;
this.currentOrder = null;
}
public synchronized void markClean() { if (status == TableStatus.DIRTY) status = TableStatus.FREE; }
public int capacity() { return capacity; }
public TableStatus status() { return status; }
}
public final class Order {
private final long id;
private final Table table;
private final long waiterId;
private final List<OrderItem> items;
private OrderState state = OrderState.PLACED;
private final Instant placedAt;
private Instant servedAt;
public Order(long id, Table t, long waiterId, List<OrderItem> items, Instant at) {
this.id = id; this.table = t; this.waiterId = waiterId;
this.items = new ArrayList<>(items); this.placedAt = at;
}
public synchronized void accept() { transition(OrderState.PLACED, OrderState.PREPARING); }
public synchronized void ready() { transition(OrderState.PREPARING, OrderState.READY); }
public synchronized void serve(Instant at) {
transition(OrderState.READY, OrderState.SERVED); this.servedAt = at;
}
public synchronized void markPaid() { transition(OrderState.SERVED, OrderState.PAID); }
public synchronized void cancel() {
if (state != OrderState.PLACED && state != OrderState.PREPARING) {
throw new IllegalStateException("Cannot cancel order in state " + state);
}
this.state = OrderState.CANCELLED;
}
private void transition(OrderState from, OrderState to) {
if (state != from) throw new IllegalStateException("Order is " + state + ", cannot move to " + to);
this.state = to;
}
public List<OrderItem> items() { return List.copyOf(items); }
public OrderState state() { return state; }
public Table table() { return table; }
}
/** Table-assignment Strategy. */
public interface TableAssignmentStrategy {
Optional<Table> assign(int partySize, List<Table> tables);
}
public final class NearestFitAssignment implements TableAssignmentStrategy {
public Optional<Table> assign(int party, List<Table> tables) {
return tables.stream()
.filter(t -> t.status() == TableStatus.FREE && t.capacity() >= party)
.min(Comparator.comparingInt(Table::capacity));
}
}
public final class PartitionFriendlyAssignment implements TableAssignmentStrategy {
/** Reserve larger tables for larger parties; pick the smallest of the smallest tier that fits. */
public Optional<Table> assign(int party, List<Table> tables) {
return tables.stream()
.filter(t -> t.status() == TableStatus.FREE && t.capacity() >= party && t.capacity() <= party + 1)
.findFirst()
.or(() -> tables.stream()
.filter(t -> t.status() == TableStatus.FREE && t.capacity() >= party)
.min(Comparator.comparingInt(Table::capacity)));
}
}
/** Bill split Strategy. */
public interface BillSplitStrategy {
Map<String, Money> split(Bill bill, List<Payer> payers);
}
public final class EqualSplit implements BillSplitStrategy {
public Map<String, Money> split(Bill b, List<Payer> payers) {
Money share = b.total().dividedBy(payers.size());
Map<String, Money> out = new LinkedHashMap<>();
for (Payer p : payers) out.put(p.id(), share);
// last payer absorbs rounding remainder
Money rounded = share.multipliedBy(payers.size());
Money remainder = b.total().minus(rounded);
if (!remainder.isZero()) {
String lastId = payers.get(payers.size() - 1).id();
out.put(lastId, out.get(lastId).plus(remainder));
}
return out;
}
}
public final class ItemisedSplit implements BillSplitStrategy {
/** Each payer pays for their own items; tax pro-rated by item subtotal. */
public Map<String, Money> split(Bill b, List<Payer> payers) {
Map<String, Money> subtotals = new LinkedHashMap<>();
for (Payer p : payers) {
Money sum = Money.ZERO;
for (OrderItem it : p.claimedItems()) sum = sum.plus(it.lineTotal());
subtotals.put(p.id(), sum);
}
Money taxRatio = b.tax().dividedBy(b.subtotal()); // scalar
Map<String, Money> out = new LinkedHashMap<>();
for (var e : subtotals.entrySet()) {
out.put(e.getKey(), e.getValue().plus(e.getValue().multipliedBy(taxRatio)));
}
return out;
}
}
/** The Restaurant orchestrates host actions through the strategies. */
public final class Restaurant {
private final List<Table> tables;
private final Menu menu;
private TableAssignmentStrategy assignment;
private final Kitchen kitchen;
public Restaurant(List<Table> t, Menu m, TableAssignmentStrategy a, Kitchen k) {
this.tables = t; this.menu = m; this.assignment = a; this.kitchen = k;
}
public Optional<Table> seatParty(int partySize) {
Optional<Table> chosen = assignment.assign(partySize, tables);
chosen.ifPresent(t -> t.seat(null)); // null until order is placed
return chosen;
}
public Order placeOrder(Table t, long waiterId, List<OrderItem> items) {
Order o = new Order(IdGen.next(), t, waiterId, items, Instant.now());
kitchen.submit(o); // façade — kitchen routing is out of scope
return o;
}
}

Notes the interviewer will look for:

  • Order.transition(from, to) is a private helper. Each public transition (accept, ready, serve, markPaid) is a guarded entry point; the state machine is enforced in one place, not scattered across the codebase.
  • cancel() is not symmetric. Cancellation is allowed only from PLACED or PREPARING; it has its own guard. This is the kind of business rule a state machine encodes and a switch statement loses.
  • Money is a real type. Even in a slice, fares and bills are not double. The Money type encapsulates currency and rounding; EqualSplit shows the rounding-remainder rule explicitly.
  • Kitchen is a façade. kitchen.submit(o) is one line and is intentionally underspecified — the kitchen’s internal routing is out of scope.
  • TableAssignmentStrategy is injected on Restaurant. Switching strategies is configuration, not code change. A high-end restaurant with table preferences uses PartitionFriendlyAssignment; a cafeteria uses NearestFitAssignment.

Trade-offs and extensions#

DecisionWhyCost if requirements change
Order state machine with private transition()One place to enforce legal transitions; cancellation has its own guardIf a new state appears (e.g. MODIFIED after a guest changes an item), the state enum and transition table grow; the shape holds.
Table-assignment as StrategyMultiple legitimate policies; venues differNone — Strategy is the right shape.
Bill split as StrategyThree policies, all legitimate; choosing at bill-print time is finePer-item rounding is the gotcha; encapsulating in the strategy keeps EqualSplit from leaking remainder logic everywhere.
Kitchen as a façadeKitchen orchestration is its own design exercise; out of scope hereIf kitchen prioritisation enters scope, promote to a full aggregate with station and ticket model.
Reservation separate from OrderThey are different aggregates with different lifecyclesNone — keep them separate.
Table.currentOrder direct pointerSingle open order per table at a time is the venue ruleIf a venue allows tab-merging (joining two tables mid-meal), promote to a Tab aggregate spanning tables.
In-process stateLLD scopePersistence: a repository per aggregate (TableRepository, OrderRepository, ReservationRepository); strategies don’t change.
Single coarse lock per Table / OrderConcurrency is bounded; venues have ~100 tablesAt scale (chain-wide events) partition further; LLD is fine.

Likely follow-up extensions and the shape of the answer:

  • Reservation no-shows and grace windows. A scheduled task transitions RESERVED → FREE after the grace window; emit a NoShowEvent for analytics. The state machine extends, the diagram does not shift.
  • Deposits and prepayment for reservations. A Payment aggregate; the reservation references it. The bill at meal end deducts the deposit.
  • Item-level state. Promote OrderItem to carry its own state machine (Placed → Cooking → Ready → Served); the order is Ready only when all items are Ready. The aggregate becomes a small Composite.
  • Loyalty discounts. A PricingStrategy on Bill runs before the split strategy; loyalty tiers are an enum on the Guest aggregate (new).
  • Online ordering and takeaway. Order.kind becomes DINE_IN | TAKEAWAY | DELIVERY; takeaway has no Table association; delivery adds a DeliveryAddress. The state machine extends with OutForDelivery.

Mock interview follow-ups#

Questions interviewers reach for and the briefest correct answer:

  • “Why is cancel() only allowed before READY?” — Because food has been plated. Cancelling after READY would either waste the food (a business decision the system should not silently take) or require an override workflow (an explicit follow-up extension). The state machine forces the conversation up to the human.
  • “Two waiters mark the same order SERVED at the same time.”Order.serve() is synchronized and goes through transition(READY, SERVED); the second caller sees state == SERVED and throws. Concurrency is correctness, not throughput, here.
  • “A party arrives without a reservation, the only free table is reserved for someone arriving in 30 minutes.” — The host’s policy decision, but the system should expose the trade-off. NearestFitAssignment filters status == FREE and ignores RESERVED; a WalkInOverflowPolicy strategy can override (seat the party with the agreement they leave by reservation time). The strategy is the right place for the policy.
  • “How would you split a bill where one item is shared by three of five payers?”ItemisedSplit requires each OrderItem to know its payer set; the line total is divided among them. If a payer set is empty (item paid by “the table”), it splits equally across all payers. The strategy holds the policy.
  • “What changes if the menu price changes mid-evening?”OrderItem snapshots the menu item price at order time. The menu’s current price is the next order’s price; existing orders are unaffected. This is the reason OrderItem is its own class and not a (MenuItem, qty) tuple.
  • “How does the kitchen know which ticket to start first?” — Out of scope. Kitchen.submit(o) is a façade; ticket prioritisation, station routing, and chef assignment are a separate design problem.
  • “What if a guest wants to add an item after the order is PREPARING?” — The clean answer is a new OrderItem appended; the order does not retreat to PLACED. The aggregate’s state stays PREPARING; the new item enters its own per-item lifecycle and the order is READY only when all items are. Promoting items to their own state machine (the extension above) makes this explicit.
  • Hotel Management System — the closest sibling round; shares the reservation-vs-stay separation and the assignment-strategy pattern.
  • Meeting Scheduler — another reservation-against-finite-resources problem; the calendar shape is similar.
  • State PatternOrder.status is the textbook finite-state machine with guarded transitions.
  • Strategy Pattern — two independent strategies (table assignment + bill split) in one design; both are textbook uses.
  • Command Pattern — for the extension where waiter actions need an undo (mis-keyed order); the moderation analogue from stack-overflow-design.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.