One table of four, one order, and four kitchen stations that must finish together: the problem where a single request fans out into tickets, and the order is "done" only when its last ticket is.
Restaurant management is a staple of machine-coding rounds because it looks like a CRUD exercise and isn't. Four things are tempting to get wrong: giving each dish its own kitchen queue so a table's food arrives in pieces, letting a reservation and a walk-in fight over a table, checking stock and then taking it in two separate steps, and splitting a bill with floats so the shares are a cent off. This lesson is built around fixing each of them.
What this round teaches that the earlier ones don't: fan-out and join. One order is split into tickets, one per station, each ticket has its own short life (queued, cooking, ready, served), and the order's own status is not stored anywhere: it is derived from its tickets. The money side reuses the rule from Splitwise (LLD-06), integer cents and the largest-remainder rule, so a bill split never loses or invents a cent; what is new is splitting by item, where a shared dish is divided too.
How the round runs#
"Design the software for a restaurant with four tables. Parties reserve or walk in. Waiters take orders; the kitchen has a grill, an oven, a fryer and a bar, each with its own screen. Some dishes run out. At the end the table splits the bill. 60 minutes, and show me an evening running."
- 0–8 min — clarify: reservations vs walk-ins, one kitchen or stations, what "ready" means, how the bill is split
- 8–20 min — the classes, and the two shapes in the problem: time slots on tables, and one order fanning out to tickets
- 20–35 min — menu with stock, the table book, then tickets and the kitchen
- 35–48 min — the bill, and a split that adds up exactly
- 48–55 min — a scripted evening, printed
- 55–60 min — follow-ups: course timing, a split with a tip, a no-show
Step 1: Clarify the requirements#
| You ask | They answer | What it changes |
|---|---|---|
| Reservations and walk-ins? | Both. A walk-in takes a table that is free for the next 90 minutes. | One reserve(party, size, start); a walk-in is a booking that starts now |
| Which table does a party get? | The smallest one that fits. | Tables sorted by seats; best fit, so a party of two does not take the six-top |
| One kitchen queue, or per station? | Per station: grill, oven, fryer, bar. | An order becomes one ticket per station |
| When is an order ready? | When every one of its tickets is ready, so the table's food arrives together. | The order's status is derived from its tickets |
| Does a station cook many things at once? | Treat it as one ticket at a time, oldest first. | A queue and a "cooking" slot per station |
| A dish with 2 portions left? | Count them. If an order needs more than is left, reject the whole order. | Stock check and take as one atomic step |
| How do they split the bill? | Equally, or by what each person ate (some dishes shared). Tax and service on top. | A SplitStrategy, and integer money |
A bill of $40.12 for three people. What does each pay?
Split equally, 4012 cents is 1337.33 each. A cashier rounds each share and charges 13.37 three times. What is wrong, and what should the three amounts be?
One strong answer
Three times 13.37 is $40.11: a cent is missing, and the restaurant is out of pocket on every such bill. Work in integer cents, give each guest the whole-cent part (1337), and hand the remaining cent (4012 − 3 × 1337 = 1) to the guest with the largest fractional remainder; on a tie, the first by name. That is the largest remainder method: here 13.38, 13.37, 13.37, always adding to exactly 4012.
Step 2: Identify the entities#
Step 3: Class diagram#
%% title: Restaurant management: the classes
classDiagram
class Restaurant {
+place_order(party, lines, now) int
+bill(party) Bill
}
class Menu {
+items dict
+stock dict
+take_all(lines) list
}
class TableBook {
+reserve(party, size, start, minutes) Table
+table_of(party) Table
+release(party)
}
class Kitchen {
+send(order_id, party, by_station, now)
+start(station, now) Ticket
+finish(station, now)
+serve(order_id, now)
+board(station) list
}
class Ticket {
+tid int
+order_id int
+station str
+state TicketState
}
class TicketState {
<<enumeration>>
QUEUED
COOKING
READY
SERVED
}
class Bill {
+subtotal int
+total int
}
class SplitStrategy {
<<interface>>
+split(bill) dict
}
class EqualSplit
class ByItemSplit
Restaurant --> Menu
Restaurant --> TableBook
Restaurant --> Kitchen
Restaurant ..> Bill
Kitchen "1" *-- "many" Ticket
Ticket --> TicketState
SplitStrategy <|.. EqualSplit
SplitStrategy <|.. ByItemSplit
SplitStrategy ..> BillThree small subsystems and one facade. TableBook knows only about time slots. Menu knows only about dishes and portions left. Kitchen knows only about tickets and stations; it never sees a price or a table. Restaurant is the one class a waiter's app talks to: it checks the party has a table, takes the stock, splits the order by station and hands the pieces to the kitchen. The bill and the split strategies are plain calculations over what was ordered.
The order itself is deliberately not a class with a status field. A ticket has a state; the order is "complete" when all its tickets are ready, and the kitchen works that out at the moment a ticket finishes. A stored Order.status would be a second copy of the same fact, and the two would drift.
Step 4: Patterns applied#
| Pattern | Where | Why it earns its place |
|---|---|---|
| Strategy | SplitStrategy / EqualSplit, ByItemSplit |
Splitting rules multiply: by item, by percentage, one person pays. Each is a new class, and the bill does not change. |
| Observer | Kitchen.listeners |
The kitchen display, the pass, a log and a customer's "your order is ready" ping all need the same events. The kitchen publishes; it does not know who listens. |
| State (as an enum) | TicketState |
A ticket moves one step forward at a time. Four values and a forward-only rule are enough; no class per state. |
| Facade | Restaurant |
A waiter's app calls one method. The order of steps (table check, stock, split by station, send) lives in one place. |
Step 5: Code#
The interfaces#
Money is an int of cents everywhere. A dish names the station that cooks it. Stock is a count of portions, or "not counted" for dishes that never run out.
# restaurant.py, part 1: menu, stock and a ticket's life
from __future__ import annotations
import threading
from abc import ABC, abstractmethod
from collections import deque
from collections.abc import Callable
from dataclasses import dataclass, field
from enum import Enum
class TicketState(Enum):
QUEUED = "queued"
COOKING = "cooking"
READY = "ready"
SERVED = "served"
ORDER_OF_STATES = [TicketState.QUEUED, TicketState.COOKING, TicketState.READY, TicketState.SERVED]
@dataclass(frozen=True)
class MenuItem:
name: str
price: int # in cents: money is never a float
station: str # which part of the kitchen cooks it
class SoldOut(Exception):
pass
class Menu:
def __init__(self):
self.items: dict[str, MenuItem] = {}
self.stock: dict[str, int | None] = {} # portions left; None = not counted
self._lock = threading.Lock()
def add(self, item: MenuItem, portions: int | None = None) -> None:
self.items[item.name] = item
self.stock[item.name] = portions
def take_all(self, lines: list[tuple[str, int]]) -> list[tuple[MenuItem, int]]:
"""All the lines or none: check every portion count first, then take them."""
with self._lock:
need: dict[str, int] = {}
for name, qty in lines:
if name not in self.items:
raise SoldOut(f"not on the menu: {name}")
need[name] = need.get(name, 0) + qty
for name, qty in need.items():
left = self.stock[name]
if left is not None and left < qty:
raise SoldOut(f"sold out: {name}")
for name, qty in need.items():
if self.stock[name] is not None:
self.stock[name] -= qty
return [(self.items[name], qty) for name, qty in lines]// Restaurant.java, part 1: menu, stock and a ticket's life
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
enum TicketState { QUEUED, COOKING, READY, SERVED }
record MenuItem(String name, int price, String station) {} // price in cents: money is never a float
record Line(String name, int qty) {}
record Taken(MenuItem item, int qty) {}
class SoldOut extends RuntimeException {
SoldOut(String message) { super(message); }
}
class Menu {
final Map<String, MenuItem> items = new HashMap<>();
final Map<String, Integer> stock = new HashMap<>(); // portions left; absent = not counted
private final ReentrantLock lock = new ReentrantLock();
void add(MenuItem item, Integer portions) {
items.put(item.name(), item);
if (portions != null) stock.put(item.name(), portions);
}
/** All the lines or none: check every portion count first, then take them. */
List<Taken> takeAll(List<Line> lines) {
lock.lock();
try {
Map<String, Integer> need = new LinkedHashMap<>();
for (Line l : lines) {
if (!items.containsKey(l.name())) throw new SoldOut("not on the menu: " + l.name());
need.merge(l.name(), l.qty(), Integer::sum);
}
for (var e : need.entrySet()) {
Integer left = stock.get(e.getKey());
if (left != null && left < e.getValue()) throw new SoldOut("sold out: " + e.getKey());
}
for (var e : need.entrySet()) {
if (stock.containsKey(e.getKey())) stock.merge(e.getKey(), -e.getValue(), Integer::sum);
}
List<Taken> out = new ArrayList<>();
for (Line l : lines) out.add(new Taken(items.get(l.name()), l.qty()));
return out;
} finally {
lock.unlock();
}
}
}// restaurant.cpp, part 1: menu, stock and a ticket's life
#include <algorithm>
#include <deque>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
enum class TicketState { Queued, Cooking, Ready, Served };
struct MenuItem {
std::string name;
int price; // in cents: money is never a float
std::string station;
};
struct Line {
std::string name;
int qty;
};
struct Taken {
MenuItem item;
int qty;
};
struct SoldOut : std::runtime_error {
using std::runtime_error::runtime_error;
};
class Menu {
public:
std::map<std::string, MenuItem> items;
std::map<std::string, int> stock; // portions left; absent = not counted
void add(const MenuItem& item, std::optional<int> portions = std::nullopt) {
items.insert_or_assign(item.name, item);
if (portions) stock[item.name] = *portions;
}
// All the lines or none: check every portion count first, then take them.
std::vector<Taken> takeAll(const std::vector<Line>& lines) {
std::lock_guard<std::mutex> g(mu_);
std::map<std::string, int> need;
for (const auto& l : lines) {
if (!items.count(l.name)) throw SoldOut("not on the menu: " + l.name);
need[l.name] += l.qty;
}
for (const auto& [name, qty] : need) {
auto it = stock.find(name);
if (it != stock.end() && it->second < qty) throw SoldOut("sold out: " + name);
}
for (const auto& [name, qty] : need) {
auto it = stock.find(name);
if (it != stock.end()) it->second -= qty;
}
std::vector<Taken> out;
for (const auto& l : lines) out.push_back({items.at(l.name), l.qty});
return out;
}
private:
std::mutex mu_;
};// restaurant.js, part 1: menu, stock and a ticket's life
const TicketState = Object.freeze({ QUEUED: "queued", COOKING: "cooking", READY: "ready", SERVED: "served" });
class MenuItem {
constructor(name, price, station) {
this.name = name;
this.price = price; // in cents: money is never a float
this.station = station; // which part of the kitchen cooks it
}
}
class SoldOut extends Error {}
class Menu {
constructor() {
this.items = new Map();
this.stock = new Map(); // portions left; absent = not counted
}
add(item, portions = null) {
this.items.set(item.name, item);
if (portions !== null) this.stock.set(item.name, portions);
}
// All the lines or none: check every portion count first, then take them.
// (One thread runs the event loop, so this check-then-take cannot interleave; in Java and C++ it holds a lock.)
takeAll(lines) {
const need = new Map();
for (const [name, qty] of lines) {
if (!this.items.has(name)) throw new SoldOut(`not on the menu: ${name}`);
need.set(name, (need.get(name) ?? 0) + qty);
}
for (const [name, qty] of need) {
if (this.stock.has(name) && this.stock.get(name) < qty) throw new SoldOut(`sold out: ${name}`);
}
for (const [name, qty] of need) {
if (this.stock.has(name)) this.stock.set(name, this.stock.get(name) - qty);
}
return lines.map(([name, qty]) => [this.items.get(name), qty]);
}
}Menu.take_all is the atomic step. It first adds up what each dish is needed (two lines for the same dish count together), checks every count, and only then takes anything. If it raises SoldOut, no portion has been spent and nothing has been queued.
Tables: a walk-in is a booking that starts now#
# part 2: tables and reservations: a walk-in is just a booking that starts now
@dataclass(frozen=True)
class Table:
tid: int
seats: int
class TableBook:
TURN = 90 # minutes a party is given a table for
def __init__(self, tables: list[Table]):
self.tables = sorted(tables, key=lambda t: (t.seats, t.tid)) # best fit first
self.bookings: dict[int, list[tuple[int, int, str]]] = {t.tid: [] for t in tables}
def _free(self, table: Table, start: int, end: int) -> bool:
return all(end <= s or start >= e for s, e, _ in self.bookings[table.tid])
def reserve(self, party: str, size: int, start: int, minutes: int = TURN) -> Table | None:
for table in self.tables: # smallest table that fits and is free for the whole slot
if table.seats >= size and self._free(table, start, start + minutes):
self.bookings[table.tid].append((start, start + minutes, party))
return table
return None
def table_of(self, party: str) -> Table | None:
for table in self.tables:
if any(p == party for _, _, p in self.bookings[table.tid]):
return table
return None
def release(self, party: str) -> None:
for tid in self.bookings:
self.bookings[tid] = [b for b in self.bookings[tid] if b[2] != party]// part 2: tables and reservations: a walk-in is just a booking that starts now
record Table(int tid, int seats) {}
record Booking(int start, int end, String party) {}
class TableBook {
final List<Table> tables = new ArrayList<>();
final Map<Integer, List<Booking>> bookings = new HashMap<>();
TableBook(List<Table> all) {
tables.addAll(all);
tables.sort(Comparator.comparingInt(Table::seats).thenComparingInt(Table::tid)); // best fit first
for (Table t : all) bookings.put(t.tid(), new ArrayList<>());
}
private boolean free(Table table, int start, int end) {
for (Booking b : bookings.get(table.tid())) if (!(end <= b.start() || start >= b.end())) return false;
return true;
}
Table reserve(String party, int size, int start) { return reserve(party, size, start, 90); }
Table reserve(String party, int size, int start, int minutes) {
for (Table table : tables) { // smallest table that fits and is free for the whole slot
if (table.seats() >= size && free(table, start, start + minutes)) {
bookings.get(table.tid()).add(new Booking(start, start + minutes, party));
return table;
}
}
return null;
}
Table tableOf(String party) {
for (Table table : tables) {
for (Booking b : bookings.get(table.tid())) if (b.party().equals(party)) return table;
}
return null;
}
void release(String party) {
for (var list : bookings.values()) list.removeIf(b -> b.party().equals(party));
}
}// part 2: tables and reservations: a walk-in is just a booking that starts now
struct Table {
int tid;
int seats;
};
struct Booking {
int start;
int end;
std::string party;
};
class TableBook {
public:
std::vector<Table> tables;
std::map<int, std::vector<Booking>> bookings;
explicit TableBook(std::vector<Table> all) : tables(std::move(all)) {
std::sort(tables.begin(), tables.end(), [](const Table& a, const Table& b) { // best fit first
return a.seats != b.seats ? a.seats < b.seats : a.tid < b.tid;
});
for (const auto& t : tables) bookings[t.tid];
}
const Table* reserve(const std::string& party, int size, int start, int minutes = 90) {
for (const auto& table : tables) { // smallest table that fits and is free for the whole slot
if (table.seats >= size && isFree(table, start, start + minutes)) {
bookings[table.tid].push_back({start, start + minutes, party});
return &table;
}
}
return nullptr;
}
const Table* tableOf(const std::string& party) const {
for (const auto& table : tables) {
for (const auto& b : bookings.at(table.tid)) if (b.party == party) return &table;
}
return nullptr;
}
void release(const std::string& party) {
for (auto& [tid, list] : bookings) {
list.erase(std::remove_if(list.begin(), list.end(), [&](const Booking& b) { return b.party == party; }), list.end());
}
}
private:
bool isFree(const Table& table, int start, int end) const {
for (const auto& b : bookings.at(table.tid)) if (!(end <= b.start || start >= b.end)) return false;
return true;
}
};// part 2: tables and reservations: a walk-in is just a booking that starts now
class Table {
constructor(tid, seats) {
this.tid = tid;
this.seats = seats;
}
}
class TableBook {
constructor(tables) {
this.tables = [...tables].sort((a, b) => a.seats - b.seats || a.tid - b.tid); // best fit first
this.bookings = new Map(tables.map((t) => [t.tid, []]));
}
isFree(table, start, end) {
return this.bookings.get(table.tid).every((b) => end <= b.start || start >= b.end);
}
reserve(party, size, start, minutes = 90) {
for (const table of this.tables) { // smallest table that fits and is free for the whole slot
if (table.seats >= size && this.isFree(table, start, start + minutes)) {
this.bookings.get(table.tid).push({ start, end: start + minutes, party });
return table;
}
}
return null;
}
tableOf(party) {
return this.tables.find((t) => this.bookings.get(t.tid).some((b) => b.party === party)) ?? null;
}
release(party) {
for (const [tid, list] of this.bookings) this.bookings.set(tid, list.filter((b) => b.party !== party));
}
}One method covers both cases. A table is free for [start, start + 90) if no existing booking overlaps that interval; two intervals overlap unless one ends before the other starts. Tables are sorted by seats then id, so the first free one that fits is the best fit. That also gives the right behaviour for the awkward case: a walk-in at t=0 cannot take a table reserved for t=60, because their slots overlap.
Tickets, stations and the display#
# part 3: one order becomes a ticket per station; the kitchen display listens
@dataclass
class Ticket:
tid: int
order_id: int
party: str
station: str
lines: list[tuple[str, int]]
state: TicketState = TicketState.QUEUED
Listener = Callable[[int, str], None] # (time, text)
class Kitchen:
def __init__(self, stations: list[str]):
self.queues: dict[str, deque[Ticket]] = {s: deque() for s in stations}
self.cooking: dict[str, Ticket | None] = {s: None for s in stations}
self.tickets: dict[int, Ticket] = {}
self.listeners: list[Listener] = []
self._next = 1
self._lock = threading.Lock()
def send(self, order_id: int, party: str, by_station: dict[str, list[tuple[str, int]]], now: int) -> list[Ticket]:
events = []
with self._lock:
made = []
for station, lines in by_station.items():
t = Ticket(self._next, order_id, party, station, lines)
self._next += 1
self.tickets[t.tid] = t
self.queues[station].append(t)
made.append(t)
events.append(f"{station}: ticket {t.tid} queued for {party}")
self._tell(now, events)
return made
def start(self, station: str, now: int) -> Ticket | None:
with self._lock:
if self.cooking[station] or not self.queues[station]:
return None # one ticket at a time per station; oldest first
t = self.queues[station].popleft()
t.state = TicketState.COOKING
self.cooking[station] = t
events = [f"{station}: cooking ticket {t.tid}"]
self._tell(now, events)
return t
def finish(self, station: str, now: int) -> None:
with self._lock:
t = self.cooking[station]
if t is None:
raise ValueError(f"{station} is not cooking anything")
t.state = TicketState.READY
self.cooking[station] = None
events = [f"{station}: ticket {t.tid} ready"]
mine = [x for x in self.tickets.values() if x.order_id == t.order_id]
if all(x.state in (TicketState.READY, TicketState.SERVED) for x in mine):
events.append(f"order {t.order_id} ({t.party}) complete: run it to the table")
self._tell(now, events)
def serve(self, order_id: int, now: int) -> None:
with self._lock:
mine = [x for x in self.tickets.values() if x.order_id == order_id]
if not mine or any(x.state is not TicketState.READY for x in mine):
raise ValueError(f"order {order_id} is not fully ready")
for x in mine:
x.state = TicketState.SERVED
events = [f"order {order_id} ({mine[0].party}) served"]
self._tell(now, events)
def board(self, station: str) -> list[str]:
with self._lock:
return [f"#{t.tid} {t.party}: " + ", ".join(f"{q} x {n}" for n, q in t.lines) for t in self.queues[station]]
def _tell(self, now: int, events: list[str]) -> None: # always outside the lock
for text in events:
for listener in self.listeners:
listener(now, text)// part 3: one order becomes a ticket per station; the kitchen display listens
class Ticket {
final int tid, orderId;
final String party, station;
final List<Line> lines;
TicketState state = TicketState.QUEUED;
Ticket(int tid, int orderId, String party, String station, List<Line> lines) {
this.tid = tid;
this.orderId = orderId;
this.party = party;
this.station = station;
this.lines = lines;
}
}
class Kitchen {
final Map<String, Deque<Ticket>> queues = new LinkedHashMap<>();
final Map<String, Ticket> cooking = new HashMap<>();
final Map<Integer, Ticket> tickets = new LinkedHashMap<>();
final List<BiConsumer<Integer, String>> listeners = new ArrayList<>(); // (time, text)
private int next = 1;
private final ReentrantLock lock = new ReentrantLock();
Kitchen(List<String> stations) {
for (String s : stations) queues.put(s, new ArrayDeque<>());
}
void send(int orderId, String party, Map<String, List<Line>> byStation, int now) {
List<String> events = new ArrayList<>();
lock.lock();
try {
for (var e : byStation.entrySet()) {
Ticket t = new Ticket(next++, orderId, party, e.getKey(), e.getValue());
tickets.put(t.tid, t);
queues.get(e.getKey()).add(t);
events.add(e.getKey() + ": ticket " + t.tid + " queued for " + party);
}
} finally {
lock.unlock();
}
tell(now, events);
}
Ticket start(String station, int now) {
List<String> events = new ArrayList<>();
Ticket t;
lock.lock();
try {
if (cooking.get(station) != null || queues.get(station).isEmpty()) return null; // one at a time; oldest first
t = queues.get(station).poll();
t.state = TicketState.COOKING;
cooking.put(station, t);
events.add(station + ": cooking ticket " + t.tid);
} finally {
lock.unlock();
}
tell(now, events);
return t;
}
void finish(String station, int now) {
List<String> events = new ArrayList<>();
lock.lock();
try {
Ticket t = cooking.get(station);
if (t == null) throw new IllegalStateException(station + " is not cooking anything");
t.state = TicketState.READY;
cooking.remove(station);
events.add(station + ": ticket " + t.tid + " ready");
boolean all = true;
for (Ticket x : tickets.values()) {
if (x.orderId == t.orderId && x.state != TicketState.READY && x.state != TicketState.SERVED) all = false;
}
if (all) events.add("order " + t.orderId + " (" + t.party + ") complete: run it to the table");
} finally {
lock.unlock();
}
tell(now, events);
}
void serve(int orderId, int now) {
List<String> events = new ArrayList<>();
lock.lock();
try {
List<Ticket> mine = new ArrayList<>();
for (Ticket x : tickets.values()) if (x.orderId == orderId) mine.add(x);
boolean ready = !mine.isEmpty();
for (Ticket x : mine) if (x.state != TicketState.READY) ready = false;
if (!ready) throw new IllegalStateException("order " + orderId + " is not fully ready");
for (Ticket x : mine) x.state = TicketState.SERVED;
events.add("order " + orderId + " (" + mine.get(0).party + ") served");
} finally {
lock.unlock();
}
tell(now, events);
}
List<String> board(String station) {
lock.lock();
try {
List<String> out = new ArrayList<>();
for (Ticket t : queues.get(station)) {
StringJoiner j = new StringJoiner(", ");
for (Line l : t.lines) j.add(l.qty() + " x " + l.name());
out.add("#" + t.tid + " " + t.party + ": " + j);
}
return out;
} finally {
lock.unlock();
}
}
private void tell(int now, List<String> events) { // always outside the lock
for (String text : events) for (var l : listeners) l.accept(now, text);
}
}// part 3: one order becomes a ticket per station; the kitchen display listens
struct Ticket {
int tid;
int orderId;
std::string party;
std::string station;
std::vector<Line> lines;
TicketState state = TicketState::Queued;
};
using Listener = std::function<void(int, const std::string&)>; // (time, text)
class Kitchen {
public:
std::vector<Listener> listeners;
explicit Kitchen(const std::vector<std::string>& stations) {
for (const auto& s : stations) queues_[s];
}
void send(int orderId, const std::string& party, const std::vector<std::pair<std::string, std::vector<Line>>>& byStation, int now) {
std::vector<std::string> events;
{
std::lock_guard<std::mutex> g(mu_);
for (const auto& [station, lines] : byStation) {
auto t = std::make_shared<Ticket>(Ticket{next_++, orderId, party, station, lines});
tickets_.push_back(t);
queues_[station].push_back(t);
events.push_back(station + ": ticket " + std::to_string(t->tid) + " queued for " + party);
}
}
tell(now, events);
}
std::shared_ptr<Ticket> start(const std::string& station, int now) {
std::vector<std::string> events;
std::shared_ptr<Ticket> t;
{
std::lock_guard<std::mutex> g(mu_);
if (cooking_[station] || queues_[station].empty()) return nullptr; // one at a time; oldest first
t = queues_[station].front();
queues_[station].pop_front();
t->state = TicketState::Cooking;
cooking_[station] = t;
events.push_back(station + ": cooking ticket " + std::to_string(t->tid));
}
tell(now, events);
return t;
}
void finish(const std::string& station, int now) {
std::vector<std::string> events;
{
std::lock_guard<std::mutex> g(mu_);
auto t = cooking_[station];
if (!t) throw std::logic_error(station + " is not cooking anything");
t->state = TicketState::Ready;
cooking_[station] = nullptr;
events.push_back(station + ": ticket " + std::to_string(t->tid) + " ready");
bool all = true;
for (const auto& x : tickets_) {
if (x->orderId == t->orderId && x->state != TicketState::Ready && x->state != TicketState::Served) all = false;
}
if (all) events.push_back("order " + std::to_string(t->orderId) + " (" + t->party + ") complete: run it to the table");
}
tell(now, events);
}
void serve(int orderId, int now) {
std::vector<std::string> events;
{
std::lock_guard<std::mutex> g(mu_);
std::vector<std::shared_ptr<Ticket>> mine;
for (const auto& x : tickets_) if (x->orderId == orderId) mine.push_back(x);
bool ready = !mine.empty();
for (const auto& x : mine) if (x->state != TicketState::Ready) ready = false;
if (!ready) throw std::logic_error("order " + std::to_string(orderId) + " is not fully ready");
for (const auto& x : mine) x->state = TicketState::Served;
events.push_back("order " + std::to_string(orderId) + " (" + mine[0]->party + ") served");
}
tell(now, events);
}
std::vector<std::string> board(const std::string& station) {
std::lock_guard<std::mutex> g(mu_);
std::vector<std::string> out;
for (const auto& t : queues_[station]) {
std::string s = "#" + std::to_string(t->tid) + " " + t->party + ": ";
for (size_t i = 0; i < t->lines.size(); i++) {
s += (i ? ", " : "") + std::to_string(t->lines[i].qty) + " x " + t->lines[i].name;
}
out.push_back(s);
}
return out;
}
private:
std::map<std::string, std::deque<std::shared_ptr<Ticket>>> queues_;
std::map<std::string, std::shared_ptr<Ticket>> cooking_;
std::vector<std::shared_ptr<Ticket>> tickets_;
int next_ = 1;
std::mutex mu_;
void tell(int now, const std::vector<std::string>& events) { // always outside the lock
for (const auto& text : events) for (const auto& l : listeners) l(now, text);
}
};// part 3: one order becomes a ticket per station; the kitchen display listens
class Ticket {
constructor(tid, orderId, party, station, lines) {
Object.assign(this, { tid, orderId, party, station, lines });
this.state = TicketState.QUEUED;
}
}
class Kitchen {
constructor(stations) {
this.queues = new Map(stations.map((s) => [s, []]));
this.cooking = new Map(stations.map((s) => [s, null]));
this.tickets = [];
this.listeners = []; // each called with (time, text)
this.next = 1;
}
send(orderId, party, byStation, now) {
const events = [];
for (const [station, lines] of byStation) {
const t = new Ticket(this.next++, orderId, party, station, lines);
this.tickets.push(t);
this.queues.get(station).push(t);
events.push(`${station}: ticket ${t.tid} queued for ${party}`);
}
this.tell(now, events);
}
start(station, now) {
if (this.cooking.get(station) || this.queues.get(station).length === 0) return null; // one at a time; oldest first
const t = this.queues.get(station).shift();
t.state = TicketState.COOKING;
this.cooking.set(station, t);
this.tell(now, [`${station}: cooking ticket ${t.tid}`]);
return t;
}
finish(station, now) {
const t = this.cooking.get(station);
if (!t) throw new Error(`${station} is not cooking anything`);
t.state = TicketState.READY;
this.cooking.set(station, null);
const events = [`${station}: ticket ${t.tid} ready`];
const mine = this.tickets.filter((x) => x.orderId === t.orderId);
if (mine.every((x) => x.state === TicketState.READY || x.state === TicketState.SERVED)) {
events.push(`order ${t.orderId} (${t.party}) complete: run it to the table`);
}
this.tell(now, events);
}
serve(orderId, now) {
const mine = this.tickets.filter((x) => x.orderId === orderId);
if (mine.length === 0 || mine.some((x) => x.state !== TicketState.READY)) throw new Error(`order ${orderId} is not fully ready`);
mine.forEach((x) => (x.state = TicketState.SERVED));
this.tell(now, [`order ${orderId} (${mine[0].party}) served`]);
}
board(station) {
return this.queues.get(station).map((t) => `#${t.tid} ${t.party}: ` + t.lines.map(([n, q]) => `${q} x ${n}`).join(", "));
}
tell(now, events) {
for (const text of events) for (const l of this.listeners) l(now, text);
}
}send makes one ticket per station and queues each on its station's own FIFO. start takes the oldest queued ticket only when the station is idle. finish marks a ticket ready and, in the same locked step, checks the order's other tickets: if all are READY or SERVED, it announces that the order is complete. serve demands that every ticket of the order is READY, so a half-finished order cannot be run out.
Listeners are called after the lock is released, so a slow screen cannot hold up the line. The display, a printer at the pass and a log can all subscribe without the kitchen knowing about them.
The bill, and a split that adds up#
# part 4: the bill, and splitting it without losing a cent
def allocate(total: int, weights: dict[str, int]) -> dict[str, int]:
"""Share `total` cents in proportion to the weights; the shares add up to exactly `total`."""
whole = sum(weights.values())
base = {k: total * w // whole for k, w in weights.items()}
left = total - sum(base.values())
by_remainder = sorted(weights, key=lambda k: (-(total * weights[k] % whole), k))
for k in by_remainder[:left]: # the biggest fractional parts get the spare cents
base[k] += 1
return base
def percent(amount: int, bps: int) -> int: # basis points, rounded half up
return (amount * bps + 5000) // 10000
@dataclass
class Bill:
lines: list[tuple[MenuItem, int]]
tax_bps: int = 800
service_bps: int = 1000
@property
def subtotal(self) -> int:
return sum(item.price * qty for item, qty in self.lines)
@property
def total(self) -> int:
return self.subtotal + percent(self.subtotal, self.tax_bps) + percent(self.subtotal, self.service_bps)
class SplitStrategy(ABC):
@abstractmethod
def split(self, bill: Bill) -> dict[str, int]: ...
class EqualSplit(SplitStrategy):
def __init__(self, guests: list[str]):
self.guests = guests
def split(self, bill: Bill) -> dict[str, int]:
return allocate(bill.total, {g: 1 for g in self.guests})
class ByItemSplit(SplitStrategy):
"""eaten: item name -> the guests who share it. Tax and service follow each guest's food."""
def __init__(self, eaten: dict[str, list[str]]):
self.eaten = eaten
def split(self, bill: Bill) -> dict[str, int]:
food: dict[str, int] = {}
for item, qty in bill.lines:
guests = self.eaten[item.name]
for g, cents in allocate(item.price * qty, {g: 1 for g in guests}).items():
food[g] = food.get(g, 0) + cents
return allocate(bill.total, food)// part 4: the bill, and splitting it without losing a cent
class Money {
/** Share `total` cents in proportion to the weights; the shares add up to exactly `total`. */
static Map<String, Integer> allocate(int total, Map<String, Integer> weights) {
int whole = 0;
for (int w : weights.values()) whole += w;
Map<String, Integer> base = new LinkedHashMap<>();
int given = 0;
for (var e : weights.entrySet()) {
int share = total * e.getValue() / whole;
base.put(e.getKey(), share);
given += share;
}
final int whole2 = whole;
List<String> order = new ArrayList<>(weights.keySet());
order.sort(Comparator.comparingInt((String k) -> -(total * weights.get(k) % whole2)).thenComparing(k -> k));
for (int i = 0; i < total - given; i++) base.merge(order.get(i), 1, Integer::sum); // biggest fractions get the spare cents
return base;
}
static int percent(int amount, int bps) { return (amount * bps + 5000) / 10000; } // basis points, half up
}
class Bill {
final List<Taken> lines;
final int taxBps = 800, serviceBps = 1000;
Bill(List<Taken> lines) { this.lines = lines; }
int subtotal() {
int s = 0;
for (Taken t : lines) s += t.item().price() * t.qty();
return s;
}
int total() { return subtotal() + Money.percent(subtotal(), taxBps) + Money.percent(subtotal(), serviceBps); }
}
interface SplitStrategy {
Map<String, Integer> split(Bill bill);
}
class EqualSplit implements SplitStrategy {
final List<String> guests;
EqualSplit(List<String> guests) { this.guests = guests; }
public Map<String, Integer> split(Bill bill) {
Map<String, Integer> w = new LinkedHashMap<>();
for (String g : guests) w.put(g, 1);
return Money.allocate(bill.total(), w);
}
}
/** eaten: item name -> the guests who share it. Tax and service follow each guest's food. */
class ByItemSplit implements SplitStrategy {
final Map<String, List<String>> eaten;
ByItemSplit(Map<String, List<String>> eaten) { this.eaten = eaten; }
public Map<String, Integer> split(Bill bill) {
Map<String, Integer> food = new LinkedHashMap<>();
for (Taken t : bill.lines) {
Map<String, Integer> w = new LinkedHashMap<>();
for (String g : eaten.get(t.item().name())) w.put(g, 1);
for (var e : Money.allocate(t.item().price() * t.qty(), w).entrySet()) food.merge(e.getKey(), e.getValue(), Integer::sum);
}
return Money.allocate(bill.total(), food);
}
}// part 4: the bill, and splitting it without losing a cent
using Shares = std::vector<std::pair<std::string, int>>; // guest -> cents, in the order given
// Share `total` cents in proportion to the weights; the shares add up to exactly `total`.
Shares allocate(int total, const Shares& weights) {
int whole = 0;
for (const auto& w : weights) whole += w.second;
Shares base;
int given = 0;
for (const auto& [k, w] : weights) {
base.push_back({k, total * w / whole});
given += total * w / whole;
}
std::vector<size_t> order(weights.size());
for (size_t i = 0; i < order.size(); i++) order[i] = i;
std::sort(order.begin(), order.end(), [&](size_t a, size_t b) { // biggest fractional parts first
int ra = total * weights[a].second % whole, rb = total * weights[b].second % whole;
return ra != rb ? ra > rb : weights[a].first < weights[b].first;
});
for (int i = 0; i < total - given; i++) base[order[i]].second += 1;
return base;
}
int percent(int amount, int bps) { return (amount * bps + 5000) / 10000; } // basis points, half up
struct Bill {
std::vector<Taken> lines;
int taxBps = 800;
int serviceBps = 1000;
int subtotal() const {
int s = 0;
for (const auto& t : lines) s += t.item.price * t.qty;
return s;
}
int total() const { return subtotal() + percent(subtotal(), taxBps) + percent(subtotal(), serviceBps); }
};
class SplitStrategy {
public:
virtual ~SplitStrategy() = default;
virtual Shares split(const Bill& bill) const = 0;
};
class EqualSplit : public SplitStrategy {
public:
explicit EqualSplit(std::vector<std::string> guests) : guests_(std::move(guests)) {}
Shares split(const Bill& bill) const override {
Shares w;
for (const auto& g : guests_) w.push_back({g, 1});
return allocate(bill.total(), w);
}
private:
std::vector<std::string> guests_;
};
// eaten: item name -> the guests who share it. Tax and service follow each guest's food.
class ByItemSplit : public SplitStrategy {
public:
explicit ByItemSplit(std::map<std::string, std::vector<std::string>> eaten) : eaten_(std::move(eaten)) {}
Shares split(const Bill& bill) const override {
Shares food;
for (const auto& t : bill.lines) {
Shares w;
for (const auto& g : eaten_.at(t.item.name)) w.push_back({g, 1});
for (const auto& [g, cents] : allocate(t.item.price * t.qty, w)) {
auto it = std::find_if(food.begin(), food.end(), [&](const auto& p) { return p.first == g; });
if (it == food.end()) food.push_back({g, cents}); else it->second += cents;
}
}
return allocate(bill.total(), food);
}
private:
std::map<std::string, std::vector<std::string>> eaten_;
};// part 4: the bill, and splitting it without losing a cent
// Share `total` cents in proportion to the weights (a Map of guest -> weight); the shares add up to exactly `total`.
function allocate(total, weights) {
const whole = [...weights.values()].reduce((a, b) => a + b, 0);
const base = new Map([...weights].map(([k, w]) => [k, Math.floor((total * w) / whole)]));
const left = total - [...base.values()].reduce((a, b) => a + b, 0);
const byRemainder = [...weights.keys()].sort(
(a, b) => ((total * weights.get(b)) % whole) - ((total * weights.get(a)) % whole) || (a < b ? -1 : 1),
);
for (const k of byRemainder.slice(0, left)) base.set(k, base.get(k) + 1); // the biggest fractional parts get the spare cents
return base;
}
const percent = (amount, bps) => Math.floor((amount * bps + 5000) / 10000); // basis points, half up
class Bill {
constructor(lines, taxBps = 800, serviceBps = 1000) {
Object.assign(this, { lines, taxBps, serviceBps });
}
get subtotal() {
return this.lines.reduce((s, [item, qty]) => s + item.price * qty, 0);
}
get total() {
return this.subtotal + percent(this.subtotal, this.taxBps) + percent(this.subtotal, this.serviceBps);
}
}
const equalWeights = (guests) => new Map(guests.map((g) => [g, 1]));
class EqualSplit {
constructor(guests) {
this.guests = guests;
}
split(bill) {
return allocate(bill.total, equalWeights(this.guests));
}
}
// eaten: item name -> the guests who share it. Tax and service follow each guest's food.
class ByItemSplit {
constructor(eaten) {
this.eaten = eaten;
}
split(bill) {
const food = new Map();
for (const [item, qty] of bill.lines) {
for (const [g, cents] of allocate(item.price * qty, equalWeights(this.eaten[item.name]))) {
food.set(g, (food.get(g) ?? 0) + cents);
}
}
return allocate(bill.total, food);
}
}allocate is the only place that divides money. Every guest gets the whole-cent part of their share; the leftover cents (never more than the number of guests) go one each to the guests with the largest fractional parts, ties broken by name so the answer is the same every run. ByItemSplit uses it twice: once to share each dish among the guests who ate it, and once to share the final total (food plus tax and service) in proportion to each guest's food. Tax and service are on the bill, rounded half up in basis points (800 = 8%), and follow the food.
The code#
The facade ties the menu, the table book and the kitchen together. Then an evening: four tables, a reservation at t=60, three walk-ins, one order that is rejected because the burgers have run out, and a kitchen run in order of time.
# part 5: the restaurant facade, and one evening
class Restaurant:
def __init__(self, menu: Menu, book: TableBook, kitchen: Kitchen):
self.menu, self.book, self.kitchen = menu, book, kitchen
self.orders: dict[int, tuple[str, list[tuple[MenuItem, int]]]] = {}
self._next = 1
def place_order(self, party: str, lines: list[tuple[str, int]], now: int) -> int:
if self.book.table_of(party) is None:
raise ValueError(f"{party} has no table")
taken = self.menu.take_all(lines) # raises SoldOut before anything is queued
order_id = self._next
self._next += 1
self.orders[order_id] = (party, taken)
by_station: dict[str, list[tuple[str, int]]] = {}
for item, qty in taken:
by_station.setdefault(item.station, []).append((item.name, qty))
self.kitchen.send(order_id, party, by_station, now)
return order_id
def bill(self, party: str) -> Bill:
lines = [line for p, taken in self.orders.values() if p == party for line in taken]
return Bill(lines)
menu = Menu()
for item, portions in [(MenuItem("Margherita", 1200, "oven"), None), (MenuItem("Burger", 1500, "grill"), 2),
(MenuItem("Fries", 400, "fryer"), None), (MenuItem("Lemonade", 350, "bar"), 10)]:
menu.add(item, portions)
book = TableBook([Table(1, 2), Table(2, 4), Table(3, 4), Table(4, 6)])
kitchen = Kitchen(["oven", "grill", "fryer", "bar"])
kitchen.listeners.append(lambda now, text: print(f" t={now:>2} {text}"))
rest = Restaurant(menu, book, kitchen)
print("Chen books 4 at t=60:", f"table {book.reserve('Chen', 4, 60).tid}")
for party, size, now in [("Ortiz", 2, 0), ("Park", 4, 0), ("Singh", 4, 5)]:
print(f"{party} walks in ({size}) at t={now}: table {book.reserve(party, size, now).tid}")
print("a table of 8:", book.reserve("Big", 8, 0))
rest.place_order("Ortiz", [("Margherita", 1), ("Fries", 1), ("Lemonade", 2)], 2)
rest.place_order("Park", [("Burger", 2), ("Fries", 1)], 3)
try:
rest.place_order("Singh", [("Lemonade", 1), ("Burger", 1)], 6)
except SoldOut as e:
print(f"Singh rejected: {e}; lemonade left: {menu.stock['Lemonade']}")
print("fryer board:", kitchen.board("fryer"))
for now, action, who in [(4, "start", "fryer"), (4, "start", "oven"), (5, "start", "bar"), (6, "start", "grill"),
(7, "finish", "fryer"), (7, "start", "fryer"), (8, "finish", "bar"), (10, "finish", "fryer"),
(14, "finish", "oven"), (15, "serve", 1), (16, "finish", "grill"), (17, "serve", 2)]:
{"start": kitchen.start, "finish": kitchen.finish, "serve": kitchen.serve}[action](who, now)
bill = rest.bill("Park")
print(f"Park's bill: subtotal {bill.subtotal}, total {bill.total}")
print("equal:", EqualSplit(["Ana", "Ben", "Cy"]).split(bill))
print("by item:", ByItemSplit({"Burger": ["Ana", "Ben"], "Fries": ["Ana", "Ben", "Cy"]}).split(bill))// part 5: the restaurant facade, and one evening
class Restaurant {
final Menu menu;
final TableBook book;
final Kitchen kitchen;
final Map<Integer, List<Taken>> orders = new LinkedHashMap<>();
final Map<Integer, String> owners = new HashMap<>();
private int next = 1;
Restaurant(Menu menu, TableBook book, Kitchen kitchen) {
this.menu = menu;
this.book = book;
this.kitchen = kitchen;
}
int placeOrder(String party, List<Line> lines, int now) {
if (book.tableOf(party) == null) throw new IllegalArgumentException(party + " has no table");
List<Taken> taken = menu.takeAll(lines); // throws SoldOut before anything is queued
int orderId = next++;
orders.put(orderId, taken);
owners.put(orderId, party);
Map<String, List<Line>> byStation = new LinkedHashMap<>();
for (Taken t : taken) byStation.computeIfAbsent(t.item().station(), k -> new ArrayList<>()).add(new Line(t.item().name(), t.qty()));
kitchen.send(orderId, party, byStation, now);
return orderId;
}
Bill bill(String party) {
List<Taken> lines = new ArrayList<>();
for (var e : orders.entrySet()) if (owners.get(e.getKey()).equals(party)) lines.addAll(e.getValue());
return new Bill(lines);
}
}
public class Main {
static String show(Map<String, Integer> m) {
StringJoiner j = new StringJoiner(", ", "{", "}");
for (var e : m.entrySet()) j.add("'" + e.getKey() + "': " + e.getValue());
return j.toString();
}
public static void main(String[] args) {
Menu menu = new Menu();
menu.add(new MenuItem("Margherita", 1200, "oven"), null);
menu.add(new MenuItem("Burger", 1500, "grill"), 2);
menu.add(new MenuItem("Fries", 400, "fryer"), null);
menu.add(new MenuItem("Lemonade", 350, "bar"), 10);
TableBook book = new TableBook(List.of(new Table(1, 2), new Table(2, 4), new Table(3, 4), new Table(4, 6)));
Kitchen kitchen = new Kitchen(List.of("oven", "grill", "fryer", "bar"));
kitchen.listeners.add((now, text) -> System.out.printf(" t=%2d %s%n", now, text));
Restaurant rest = new Restaurant(menu, book, kitchen);
System.out.println("Chen books 4 at t=60: table " + book.reserve("Chen", 4, 60).tid());
String[] names = {"Ortiz", "Park", "Singh"};
int[][] walk = {{2, 0}, {4, 0}, {4, 5}};
for (int i = 0; i < 3; i++) {
System.out.println(names[i] + " walks in (" + walk[i][0] + ") at t=" + walk[i][1] + ": table " + book.reserve(names[i], walk[i][0], walk[i][1]).tid());
}
System.out.println("a table of 8: " + (book.reserve("Big", 8, 0) == null ? "None" : "some table"));
rest.placeOrder("Ortiz", List.of(new Line("Margherita", 1), new Line("Fries", 1), new Line("Lemonade", 2)), 2);
rest.placeOrder("Park", List.of(new Line("Burger", 2), new Line("Fries", 1)), 3);
try {
rest.placeOrder("Singh", List.of(new Line("Lemonade", 1), new Line("Burger", 1)), 6);
} catch (SoldOut e) {
System.out.println("Singh rejected: " + e.getMessage() + "; lemonade left: " + menu.stock.get("Lemonade"));
}
List<String> fryer = new ArrayList<>();
for (String s : kitchen.board("fryer")) fryer.add("'" + s + "'");
System.out.println("fryer board: " + fryer);
Object[][] script = {{4, "start", "fryer"}, {4, "start", "oven"}, {5, "start", "bar"}, {6, "start", "grill"},
{7, "finish", "fryer"}, {7, "start", "fryer"}, {8, "finish", "bar"}, {10, "finish", "fryer"},
{14, "finish", "oven"}, {15, "serve", 1}, {16, "finish", "grill"}, {17, "serve", 2}};
for (Object[] step : script) {
int now = (Integer) step[0];
switch ((String) step[1]) {
case "start" -> kitchen.start((String) step[2], now);
case "finish" -> kitchen.finish((String) step[2], now);
default -> kitchen.serve((Integer) step[2], now);
}
}
Bill bill = rest.bill("Park");
System.out.println("Park's bill: subtotal " + bill.subtotal() + ", total " + bill.total());
System.out.println("equal: " + show(new EqualSplit(List.of("Ana", "Ben", "Cy")).split(bill)));
System.out.println("by item: " + show(new ByItemSplit(Map.of("Burger", List.of("Ana", "Ben"), "Fries", List.of("Ana", "Ben", "Cy"))).split(bill)));
}
}// part 5: the restaurant facade, and one evening
class Restaurant {
public:
Menu& menu;
TableBook& book;
Kitchen& kitchen;
Restaurant(Menu& m, TableBook& b, Kitchen& k) : menu(m), book(b), kitchen(k) {}
int placeOrder(const std::string& party, const std::vector<Line>& lines, int now) {
if (!book.tableOf(party)) throw std::invalid_argument(party + " has no table");
auto taken = menu.takeAll(lines); // throws SoldOut before anything is queued
int orderId = next_++;
orders_.push_back({party, taken});
std::vector<std::pair<std::string, std::vector<Line>>> byStation;
for (const auto& t : taken) {
auto it = std::find_if(byStation.begin(), byStation.end(), [&](const auto& p) { return p.first == t.item.station; });
if (it == byStation.end()) { byStation.push_back({t.item.station, {}}); it = byStation.end() - 1; }
it->second.push_back({t.item.name, t.qty});
}
kitchen.send(orderId, party, byStation, now);
return orderId;
}
Bill bill(const std::string& party) const {
Bill b;
for (const auto& [p, taken] : orders_) if (p == party) b.lines.insert(b.lines.end(), taken.begin(), taken.end());
return b;
}
private:
std::vector<std::pair<std::string, std::vector<Taken>>> orders_;
int next_ = 1;
};
std::string show(const Shares& shares) {
std::string s = "{";
for (size_t i = 0; i < shares.size(); i++) s += (i ? ", '" : "'") + shares[i].first + "': " + std::to_string(shares[i].second);
return s + "}";
}
int main() {
Menu menu;
menu.add({"Margherita", 1200, "oven"});
menu.add({"Burger", 1500, "grill"}, 2);
menu.add({"Fries", 400, "fryer"});
menu.add({"Lemonade", 350, "bar"}, 10);
TableBook book({{1, 2}, {2, 4}, {3, 4}, {4, 6}});
Kitchen kitchen({"oven", "grill", "fryer", "bar"});
kitchen.listeners.push_back([](int now, const std::string& text) {
std::cout << " t=" << (now < 10 ? " " : "") << now << " " << text << "\n";
});
Restaurant rest(menu, book, kitchen);
std::cout << "Chen books 4 at t=60: table " << book.reserve("Chen", 4, 60)->tid << "\n";
struct Walk { std::string party; int size; int now; };
for (const auto& w : std::vector<Walk>{{"Ortiz", 2, 0}, {"Park", 4, 0}, {"Singh", 4, 5}}) {
std::cout << w.party << " walks in (" << w.size << ") at t=" << w.now << ": table " << book.reserve(w.party, w.size, w.now)->tid << "\n";
}
std::cout << "a table of 8: " << (book.reserve("Big", 8, 0) ? "some table" : "None") << "\n";
rest.placeOrder("Ortiz", {{"Margherita", 1}, {"Fries", 1}, {"Lemonade", 2}}, 2);
rest.placeOrder("Park", {{"Burger", 2}, {"Fries", 1}}, 3);
try {
rest.placeOrder("Singh", {{"Lemonade", 1}, {"Burger", 1}}, 6);
} catch (const SoldOut& e) {
std::cout << "Singh rejected: " << e.what() << "; lemonade left: " << menu.stock["Lemonade"] << "\n";
}
std::string board = "[";
auto fryer = kitchen.board("fryer");
for (size_t i = 0; i < fryer.size(); i++) board += (i ? ", '" : "'") + fryer[i] + "'";
std::cout << "fryer board: " << board << "]\n";
struct Step { int now; std::string action; std::string station; int order; };
for (const auto& s : std::vector<Step>{{4, "start", "fryer", 0}, {4, "start", "oven", 0}, {5, "start", "bar", 0}, {6, "start", "grill", 0},
{7, "finish", "fryer", 0}, {7, "start", "fryer", 0}, {8, "finish", "bar", 0}, {10, "finish", "fryer", 0},
{14, "finish", "oven", 0}, {15, "serve", "", 1}, {16, "finish", "grill", 0}, {17, "serve", "", 2}}) {
if (s.action == "start") kitchen.start(s.station, s.now);
else if (s.action == "finish") kitchen.finish(s.station, s.now);
else kitchen.serve(s.order, s.now);
}
Bill bill = rest.bill("Park");
std::cout << "Park's bill: subtotal " << bill.subtotal() << ", total " << bill.total() << "\n";
std::cout << "equal: " << show(EqualSplit({"Ana", "Ben", "Cy"}).split(bill)) << "\n";
std::cout << "by item: " << show(ByItemSplit({{"Burger", {"Ana", "Ben"}}, {"Fries", {"Ana", "Ben", "Cy"}}}).split(bill)) << "\n";
}// part 5: the restaurant facade, and one evening
class Restaurant {
constructor(menu, book, kitchen) {
Object.assign(this, { menu, book, kitchen });
this.orders = new Map(); // order id -> [party, taken lines]
this.next = 1;
}
placeOrder(party, lines, now) {
if (!this.book.tableOf(party)) throw new Error(`${party} has no table`);
const taken = this.menu.takeAll(lines); // throws SoldOut before anything is queued
const orderId = this.next++;
this.orders.set(orderId, [party, taken]);
const byStation = new Map();
for (const [item, qty] of taken) {
if (!byStation.has(item.station)) byStation.set(item.station, []);
byStation.get(item.station).push([item.name, qty]);
}
this.kitchen.send(orderId, party, byStation, now);
return orderId;
}
bill(party) {
return new Bill([...this.orders.values()].filter(([p]) => p === party).flatMap(([, taken]) => taken));
}
}
const show = (m) => "{" + [...m].map(([k, v]) => `'${k}': ${v}`).join(", ") + "}";
const menu = new Menu();
menu.add(new MenuItem("Margherita", 1200, "oven"));
menu.add(new MenuItem("Burger", 1500, "grill"), 2);
menu.add(new MenuItem("Fries", 400, "fryer"));
menu.add(new MenuItem("Lemonade", 350, "bar"), 10);
const book = new TableBook([new Table(1, 2), new Table(2, 4), new Table(3, 4), new Table(4, 6)]);
const kitchen = new Kitchen(["oven", "grill", "fryer", "bar"]);
kitchen.listeners.push((now, text) => console.log(` t=${String(now).padStart(2)} ${text}`));
const rest = new Restaurant(menu, book, kitchen);
console.log("Chen books 4 at t=60:", `table ${book.reserve("Chen", 4, 60).tid}`);
for (const [party, size, now] of [["Ortiz", 2, 0], ["Park", 4, 0], ["Singh", 4, 5]]) {
console.log(`${party} walks in (${size}) at t=${now}: table ${book.reserve(party, size, now).tid}`);
}
console.log("a table of 8:", book.reserve("Big", 8, 0) ?? "None");
rest.placeOrder("Ortiz", [["Margherita", 1], ["Fries", 1], ["Lemonade", 2]], 2);
rest.placeOrder("Park", [["Burger", 2], ["Fries", 1]], 3);
try {
rest.placeOrder("Singh", [["Lemonade", 1], ["Burger", 1]], 6);
} catch (e) {
if (!(e instanceof SoldOut)) throw e;
console.log(`Singh rejected: ${e.message}; lemonade left: ${menu.stock.get("Lemonade")}`);
}
console.log("fryer board:", "[" + kitchen.board("fryer").map((s) => `'${s}'`).join(", ") + "]");
for (const [now, action, who] of [[4, "start", "fryer"], [4, "start", "oven"], [5, "start", "bar"], [6, "start", "grill"],
[7, "finish", "fryer"], [7, "start", "fryer"], [8, "finish", "bar"], [10, "finish", "fryer"],
[14, "finish", "oven"], [15, "serve", 1], [16, "finish", "grill"], [17, "serve", 2]]) {
kitchen[action](who, now);
}
const bill = rest.bill("Park");
console.log(`Park's bill: subtotal ${bill.subtotal}, total ${bill.total}`);
console.log("equal:", show(new EqualSplit(["Ana", "Ben", "Cy"]).split(bill)));
console.log("by item:", show(new ByItemSplit({ Burger: ["Ana", "Ben"], Fries: ["Ana", "Ben", "Cy"] }).split(bill)));Chen books 4 at t=60: table 2
Ortiz walks in (2) at t=0: table 1
Park walks in (4) at t=0: table 3
Singh walks in (4) at t=5: table 4
a table of 8: None
t= 2 oven: ticket 1 queued for Ortiz
t= 2 fryer: ticket 2 queued for Ortiz
t= 2 bar: ticket 3 queued for Ortiz
t= 3 grill: ticket 4 queued for Park
t= 3 fryer: ticket 5 queued for Park
Singh rejected: sold out: Burger; lemonade left: 8
fryer board: ['#2 Ortiz: 1 x Fries', '#5 Park: 1 x Fries']
t= 4 fryer: cooking ticket 2
t= 4 oven: cooking ticket 1
t= 5 bar: cooking ticket 3
t= 6 grill: cooking ticket 4
t= 7 fryer: ticket 2 ready
t= 7 fryer: cooking ticket 5
t= 8 bar: ticket 3 ready
t=10 fryer: ticket 5 ready
t=14 oven: ticket 1 ready
t=14 order 1 (Ortiz) complete: run it to the table
t=15 order 1 (Ortiz) served
t=16 grill: ticket 4 ready
t=16 order 2 (Park) complete: run it to the table
t=17 order 2 (Park) served
Park's bill: subtotal 3400, total 4012
equal: {'Ana': 1338, 'Ben': 1337, 'Cy': 1337}
by item: {'Ana': 1928, 'Ben': 1927, 'Cy': 157}Read the output in three bands. Chen's booking at t=60 pushes the walk-ins off table 2: Park (4) gets table 3, and Singh (4) gets the six-top because the other four-tops are taken. Singh's order is rejected as a whole ("sold out: Burger") and the lemonade count stays at 8, the 10 minus Ortiz's two. Ortiz's order was split across three stations and completes at t=14, when the oven, the slowest, finishes; it is served at t=15. The bill is 3400 in food, 272 tax and 340 service: 4012. Split equally, Ana gets the spare cent; split by what each ate, the shares are 1928, 1927 and 157.
Step 6: Concurrency and edge cases#
Waiters take orders on many threads while station screens advance tickets. Three small locks, none held across a callback: the Menu lock makes "check every dish, then take them" one step; the Kitchen lock covers queues, ticket states and the order-completion check; listeners are called after it is released.
- Two waiters order the last burger at once: the menu lock makes one succeed and the other get
SoldOut - An order with the same dish on two lines: count them together before comparing with stock
- A sold-out dish in a big order: reject the whole order; never queue half and strand the table
- A walk-in that would collide with a reservation 30 minutes later: it does not get that table, because the 90-minute slots overlap
- A party bigger than any table:
reservereturns nothing; the host decides whether to join tables servebefore every station is done: refuse; food for one table goes out together- A station finishing with nothing cooking: an error, not a silent no-op
- A bill that does not divide: spare cents go to the largest remainders, so the total is always exact
Step 7: Follow-ups and the interview checklist#
- "Courses: starters first, mains after." Give each line a course number.
sendcreates tickets for the next course only when the previous one is served, or the pass fires them on request. A ticket's state machine is unchanged. - "Add a tip, and a split where one person pays for two."
percenthandles the tip like tax. For paying for two,ByItemSplitor a newWeightedSplitgives that guest weight 2.allocatealready works for any weights. - "A no-show." A booking holds the table until
start + 15; after thatreleasefrees it.TableBookneeds only a clock to compare with. - "Prioritise a rush ticket." Replace the station's deque with a priority queue keyed by (priority, time of order).
startstill takes the head. - "Mark a dish unavailable (86'd)." Set its stock to 0. Orders that need it are rejected by the same check; nothing else changes.
- "Cancel an item after sending." Only while its ticket is
QUEUED; remove the line, and give back its portions. Once cooking, it is the manager's call.
Interview checklist#
Tick these off before you say done.
- I asked whether the kitchen has stations, and made one order into one ticket per station.
- I derived the order's readiness from its tickets rather than storing a second status that can drift.
- I modelled a reservation as an interval and a walk-in as a booking starting now, with one overlap check.
- I picked the best-fit table by sorting on seats, so a pair does not occupy the six-top.
- I checked every dish's portions before taking any, so a sold-out order spends no stock and queues nothing.
- I held a lock across that check-then-take, and across the kitchen's state changes.
- I notified the display listeners after releasing the kitchen lock.
- I kept money in integer cents and split it with the largest-remainder rule, so the shares add up to the bill.
- I put the split behind a
SplitStrategy, and put tax and service on the bill, not in the split. - I ran an evening with injected times, not
sleep, and printed the events.
Common mistakes#
- One queue for the whole kitchen: the fryer waits behind the oven's twenty-minute pizza
- A stored
Order.statusupdated by hand, which disagrees with the tickets the first time one is missed - Check stock, then take it in two steps, so two waiters sell the same last portion
- Floats for money: 0.1 + 0.2 on a bill, and a split that is off by a cent
- Separate code for a walk-in and a reservation, so they double-book a table
- Time from the wall clock: a 90-minute table slot you cannot test without waiting
- A
Restaurantgod class that holds the tables, the stock, the tickets and the arithmetic
Restaurant management
Q1/3 Why is an order's "ready" status derived from its tickets instead of stored on the order?
Answer & explanation
Answer: B
The tickets are the source of truth. When the last one becomes ready the kitchen announces the order, and nothing needs to be kept in step by hand.
Q2/3 Four tables: a 2-top, two 4-tops, a 6-top. A party of 4 walks in at t=0, and a 4-top is booked for t=60. Which is the best table?
Answer & explanation
Answer: C
The slot is [0, 90). The booked table overlaps from t=60, so it is out; among the rest, the smallest that fits wins.
Q3/3 A bill of 4012 cents is split three ways. What does the largest-remainder rule do?
Answer & explanation
Answer: C
4012 / 3 = 1337 remainder 1, so one guest pays 1338 and two pay 1337, and the three add up to exactly 4012.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


