Design Restaurant Management
A hostess station has a magnetic floor chart pinned to the wall, one little rectangle for each table. Every rectangle holds a colored chip — green for empty, yellow for ordering, red for cooking, blue for eating, gray for paid. The hostess never asks the floor a question. She looks at the chart, sees the chip, and knows what is allowed to happen next at that table. A waiter cannot serve food to a table whose chip is green, because the chip says nobody is there. The cook cannot start cooking a ticket for a table whose chip is yellow, because the order has not been written down yet. The whole restaurant runs on the chips, and the chips only change when somebody flips them on purpose.

The interview problem asks you to write the chips. Restaurant POS systems in the 1970s used carbon-paper tickets and a wheel above the kitchen pass — Gene Mosher's ViewTouch shipped a graphical version in 1986 on a 386 running color X11, the first time a waiter touched a screen to fire a ticket. Before that the bottleneck was that two people could change the same paper at the same time and the cook would make pizza for a party that had already left. The fix Mosher shipped was that the screen owned the truth and the staff changed it through buttons that knew which state the table was in. A button to fire an order was dead until the table was seated. A button to print the check was dead until the food went out. Every dead button was the floor chart saying "wrong chip." Rust enums and match give you the same dead-button discipline in 50 lines of code.
Start with the chips and the events that flip them.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MenuItem {
Pizza,
Salad,
Soup,
Coffee,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Status {
Ordering,
Cooking,
Eating,
Paid,
}
#[derive(Debug, Clone)]
enum Table {
Empty,
Seated {
party_size: u8,
order: Vec<MenuItem>,
status: Status,
},
}
#[derive(Debug, Clone)]
enum Event {
Seat { table: usize, party_size: u8 },
Order { table: usize, items: Vec<MenuItem> },
Serve { table: usize },
Pay { table: usize },
}MenuItem is exactly 4 things — Pizza, Salad, Soup, Coffee. Status is exactly 4 chips — Ordering, Cooking, Eating, Paid. Table is either Empty or Seated with a party size, an order, and a status. The variant payload is the move that separates a Rust answer from a Java one. A Java table holds nullable fields and a boolean is_seated, and a sloppy caller reads party_size while is_seated is false and gets 0. A Rust table cannot expose party_size until the variant is Seated, because the variant owns the field. The compiler will not let you reach past the chip to the fields under it.
Event is the menu of buttons the hostess and waiter can press. Seat, Order, Serve, Pay. That is it. The whole restaurant is 4 verbs. Anything the front-of-house wants to do has to map to one of these or it does not happen.

The hard knob is handle. It is the magnetic chart turned into a match.
struct Restaurant {
tables: Vec<(u8, Table)>,
}
impl Restaurant {
fn new() -> Self {
Self {
tables: vec![
(2, Table::Empty),
(4, Table::Empty),
(6, Table::Empty),
],
}
}
fn handle(&mut self, event: Event) -> Result<String, &'static str> {
match event {
Event::Seat { table, party_size } => {
let (cap, slot) = self.slot(table)?;
if !matches!(slot, Table::Empty) {
return Err("table not empty");
}
if party_size > cap {
return Err("party too big for table");
}
*slot = Table::Seated {
party_size,
order: Vec::new(),
status: Status::Ordering,
};
Ok(format!("seated {party_size} at table {table}"))
}
Event::Order { table, items } => {
let (_, slot) = self.slot(table)?;
match slot {
Table::Seated { order, status, .. }
if *status == Status::Ordering =>
{
*order = items;
*status = Status::Cooking;
Ok(format!("order sent to kitchen for table {table}"))
}
_ => Err("table not ordering"),
}
}
Event::Serve { table } => {
let (_, slot) = self.slot(table)?;
match slot {
Table::Seated { status, .. } if *status == Status::Cooking => {
*status = Status::Eating;
Ok(format!("served table {table}"))
}
_ => Err("nothing cooking for that table"),
}
}
Event::Pay { table } => {
let (_, slot) = self.slot(table)?;
match slot {
Table::Seated { status, order, .. }
if *status == Status::Eating =>
{
let total: u32 = order.iter().copied().map(price).sum();
*status = Status::Paid;
Ok(format!("table {table} paid ${total}"))
}
_ => Err("table not eating yet"),
}
}
}
}
fn slot(&mut self, table: usize) -> Result<(u8, &mut Table), &'static str> {
if table == 0 || table > self.tables.len() {
return Err("no such table");
}
let (cap, slot) = &mut self.tables[table - 1];
Ok((*cap, slot))
}
fn kitchen(&self) -> Vec<usize> {
let mut queue = Vec::new();
for (i, (_, slot)) in self.tables.iter().enumerate() {
if let Table::Seated { status: Status::Cooking, .. } = slot {
queue.push(i + 1);
}
}
queue
}
}Read handle from the top. The Seat arm looks up the table, refuses if the chip is not green, refuses if the party is bigger than the seats, then flips the chip to yellow and writes the party size and an empty order. The Order arm refuses if the chip is not yellow — the only state from which an order can be written down. The Serve arm refuses if the chip is not red. The Pay arm refuses if the chip is not blue, and when it does fire it walks the order and sums the prices through the price helper. Every arm returns Result<String, &'static str>, so a refusal is a value the caller can see, not a panic that kills the program.
The 3 hardcoded tables — capacity 2, 4, 6 — live in Restaurant::new so the binary is deterministic. The slot helper turns a 1-based table number from the outside world into a &mut Table inside the vector and returns an error if the number is bogus. Centralizing the lookup means every arm rejects bad table numbers the same way without copy-pasting the bounds check.
fn price(item: MenuItem) -> u32 {
match item {
MenuItem::Pizza => 12,
MenuItem::Salad => 8,
MenuItem::Soup => 6,
MenuItem::Coffee => 3,
}
}
fn glyph(item: MenuItem) -> char {
match item {
MenuItem::Pizza => 'P',
MenuItem::Salad => 'S',
MenuItem::Soup => 'U',
MenuItem::Coffee => 'C',
}
}The menu is just a match from item to price and another from item to a single letter for the floor chart. Keeping price separate from the variant is the Mosher discipline — the kitchen never has to know what the dish costs, the cashier never has to know how the dish is plated, and changing a price never risks changing the cook's logic.
Walk the chips through a real night and watch the floor.
fn render(r: &Restaurant) -> String {
let mut out = String::new();
out.push_str(" tables:\n");
for (i, (cap, slot)) in r.tables.iter().enumerate() {
let id = i + 1;
match slot {
Table::Empty => {
out.push_str(&format!(" T{id} (cap {cap}) | empty\n"));
}
Table::Seated { party_size, order, status } => {
let mut order_str = String::new();
for item in order {
order_str.push(glyph(*item));
}
if order_str.is_empty() {
order_str.push('-');
}
let tag = match status {
Status::Ordering => "ordering",
Status::Cooking => "cooking",
Status::Eating => "eating",
Status::Paid => "paid",
};
out.push_str(&format!(
" T{id} (cap {cap}) | party {party_size} | order [{order_str}] | {tag}\n"
));
}
}
}
let queue = r.kitchen();
if queue.is_empty() {
out.push_str(" kitchen queue: (empty)\n");
} else {
let mut tickets = String::new();
for (i, t) in queue.iter().enumerate() {
if i > 0 {
tickets.push_str(", ");
}
tickets.push_str(&format!("T{t}"));
}
out.push_str(&format!(" kitchen queue: {tickets}\n"));
}
out
}render prints the chart and then the kitchen queue. The queue is just whichever tables currently have a red chip. The cook does not maintain a separate list — there is no separate list to keep in sync. The truth is the chips, and the queue is a derived view of the chips. The day someone changes a chip without going through handle, the queue is wrong, which is the day you regret writing the chip-flip outside of handle.
fn run() {
let mut r = Restaurant::new();
let script = vec![
Event::Seat { table: 1, party_size: 2 },
Event::Seat { table: 2, party_size: 4 },
Event::Order { table: 1, items: vec![MenuItem::Pizza, MenuItem::Coffee] },
Event::Order { table: 2, items: vec![MenuItem::Salad, MenuItem::Soup, MenuItem::Coffee] },
Event::Serve { table: 1 },
Event::Pay { table: 1 },
Event::Serve { table: 2 },
];
println!("menu: Pizza=$12 Salad=$8 Soup=$6 Coffee=$3");
println!("--- open ---");
print!("{}", render(&r));
println!();
for (i, event) in script.iter().enumerate() {
let outcome = match r.handle(event.clone()) {
Ok(msg) => format!("ok: {msg}"),
Err(msg) => format!("err: {msg}"),
};
println!("event {}: {:?}", i + 1, event);
println!(" {outcome}");
print!("{}", render(&r));
println!();
}
let early_pay = r.handle(Event::Pay { table: 3 });
println!("pay empty table 3: {:?}", early_pay);
let oversize = r.handle(Event::Seat { table: 1, party_size: 5 });
println!("seat 5 at table 1: {:?}", oversize);
let ghost = r.handle(Event::Seat { table: 9, party_size: 2 });
println!("seat at table 9: {:?}", ghost);
}The script seats 2 tables, sends both orders to the kitchen, serves table 1, takes the check from table 1, then serves table 2. After the script, 3 dead-button presses prove the chart's Err arms — paying a still-empty table, seating a 5-top at a 2-seat table, and seating at a table that does not exist.
menu: Pizza=$12 Salad=$8 Soup=$6 Coffee=$3
--- open ---
tables:
T1 (cap 2) | empty
T2 (cap 4) | empty
T3 (cap 6) | empty
kitchen queue: (empty)
event 1: Seat { table: 1, party_size: 2 }
ok: seated 2 at table 1
tables:
T1 (cap 2) | party 2 | order [-] | ordering
T2 (cap 4) | empty
T3 (cap 6) | empty
kitchen queue: (empty)
event 2: Seat { table: 2, party_size: 4 }
ok: seated 4 at table 2
tables:
T1 (cap 2) | party 2 | order [-] | ordering
T2 (cap 4) | party 4 | order [-] | ordering
T3 (cap 6) | empty
kitchen queue: (empty)
event 3: Order { table: 1, items: [Pizza, Coffee] }
ok: order sent to kitchen for table 1
tables:
T1 (cap 2) | party 2 | order [PC] | cooking
T2 (cap 4) | party 4 | order [-] | ordering
T3 (cap 6) | empty
kitchen queue: T1
event 4: Order { table: 2, items: [Salad, Soup, Coffee] }
ok: order sent to kitchen for table 2
tables:
T1 (cap 2) | party 2 | order [PC] | cooking
T2 (cap 4) | party 4 | order [SUC] | cooking
T3 (cap 6) | empty
kitchen queue: T1, T2
event 5: Serve { table: 1 }
ok: served table 1
tables:
T1 (cap 2) | party 2 | order [PC] | eating
T2 (cap 4) | party 4 | order [SUC] | cooking
T3 (cap 6) | empty
kitchen queue: T2
event 6: Pay { table: 1 }
ok: table 1 paid $15
tables:
T1 (cap 2) | party 2 | order [PC] | paid
T2 (cap 4) | party 4 | order [SUC] | cooking
T3 (cap 6) | empty
kitchen queue: T2
event 7: Serve { table: 2 }
ok: served table 2
tables:
T1 (cap 2) | party 2 | order [PC] | paid
T2 (cap 4) | party 4 | order [SUC] | eating
T3 (cap 6) | empty
kitchen queue: (empty)
pay empty table 3: Err("table not eating yet")
seat 5 at table 1: Err("table not empty")
seat at table 9: Err("no such table")Read down the output and the chart moves. The open frame shows 3 empty tables and an empty kitchen queue. Event 1 seats 2 at table 1 — the chip flips to ordering and the order is [-] because no items have been written down. Event 3 writes the order Pizza + Coffee — the chip flips to cooking and table 1 appears in the kitchen queue. Event 4 sends table 2's order — the queue is now T1, T2. Event 5 serves table 1 — the chip flips to eating, table 1 leaves the queue, table 2 stays in it. Event 6 takes the check — the chip flips to paid, the total is 15 dollars (12 for the pizza plus 3 for the coffee), the kitchen still has table 2. Event 7 serves table 2 and the kitchen empties. The 3 trailing failures are the dead buttons — table not eating yet, table not empty, no such table — each returned as a value the caller will see and the program continued running through all of them.
One question worth asking — why does paying require the chip to be on eating and not on cooking? Look at handle. The pay arm matches Status::Eating. A real diner has not yet decided to add a coffee while the entree is being plated, so taking the check during cooking would lock the order before the table got a chance to add to it. The chart enforces a rule about restaurants, not about code — the rule lives in exactly one arm, in exactly one match, and no future change can reach around it.
The thing this design cannot do is run on its own without somebody scripting the events. The script is a hardcoded list inside main. Swap the list for a function that reads button presses from a touch screen or a waiter's tablet and nothing in Restaurant has to change — but until that happens the floor chart is a museum piece, and the next bottleneck is how to feed it live input the moment the lesson rules allow the binary to listen.