Restaurant Management
Design the system behind a dine-in restaurant: seat a party at a table, take an order off the menu, fire it to the kitchen, and serve it. The prompt has three actors - host, server, kitchen - who each touch the same order at a different point in its life, which is exactly where a design either stays clean or turns into one class with everyone's logic crammed inside it.
Requirements
Functional
- A
TableisFREE,RESERVED, orOCCUPIED; seating a party changes it toOCCUPIEDand clearing it changes it back toFREE. - An
Orderis opened against an occupied table and holds a list ofOrderItems, each referencing aMenuItemand a quantity. - Once submitted, an order moves
PLACED -> IN_KITCHEN -> READY -> SERVED; items cannot be added after it leavesPLACED. - The kitchen sees only items it needs to prepare, grouped by station (grill, cold, dessert), not the whole order as a flat undifferentiated list.
Non-functional
- A
MenuItemgoing 86'd (out of stock) mid-service must block new orders for it without the table or order code needing to know inventory exists. - Kitchen station routing (today: 3 fixed stations) must extend to new stations without
editing
OrderorOrderItem.
Design
Order is the object that outlives a single interaction between any two actors: the host
opens the table, the server builds and submits the order, the kitchen advances its status,
and the server serves it - Order is the one thing all four steps read and mutate,
exactly the role Ticket plays in Parking Lot as the
object that carries state across a gap in time. Each MenuItem carries its own
station, so grouping for the kitchen view is a partition, not a lookup table maintained
somewhere else.
Every arrow above moves forward and none loop back - addItem() is only legal while the
order is still PLACED, which the diagram makes visible as "there is exactly one state
with an edge this method could ever succeed from." TableStatus (FREE/RESERVED/
OCCUPIED) is not diagrammed here on purpose: the code only ever moves a table between
FREE and OCCUPIED (seatParty()/clear()), so RESERVED is a declared value with no
transition into or out of it in this implementation - drawing it would show a reservation
flow the code doesn't actually have.
- 1Seating only ever touches the table - it has no idea an order will follow.
- 2The server opens an order against the now-occupied table; this is the object every later actor shares.
- 3Each add checks the order is still PLACED and the item is still available, both inside this one call.
- 4Submitting flips the order to IN_KITCHEN; addItem() is no longer legal from this point on.
- 5The kitchen asks for its own view of the same order - grouped by station, never the whole flat list.
- 6The kitchen advances status when done; it never edits the item list, only the status it owns the transition into.
Availability is a property of MenuItem, checked once at the moment an item is added to
an order - the table and the order status machinery never reference stock at all.
Class diagram
Code
Design decisions
OrderStatustransitions are enforced onOrder, and adding an item is only legal inPLACED. Once an order reaches the kitchen, the printed ticket is the source of truth for what's cooking; lettingaddItem()succeed afterIN_KITCHENwould silently desync the two. Checking status insideaddItem()itself means there's no code path that can add an item to a submitted order, not even a mistake in a future caller.MenuItem.availableis a flag the item owns, checked ataddItem()time - not a separate inventory serviceOrdercalls into. For this scope, availability is binary and item-local, so putting it on the item keeps the dependency one-directional:Orderreads a field onMenuItemit already holds a reference to, rather than reaching out to a new subsystem.- Kitchen station grouping is a
groupByStation()query overOrderItem.menuItem.station, not a field that lives onOrder. The order itself has no concept of stations; the grouping is a view the kitchen asks for, computed from data that's already there. Adding a fourth station is a new enum value, not a new field anywhere. - What's missing for a real system: splitting a check across multiple diners at one
table needs
Orderto support partial-item ownership this design doesn't model (every item here belongs to the table's one order), and course timing (appetizers fire to the kitchen before entrees) needs per-item submission rather than the whole order moving toIN_KITCHENat once - both left out to keep the four-status lifecycle the whole focus.
Common follow-ups
- How would you support course timing (fire appetizers before entrees)? Give each
OrderItemits own status instead of the wholeOrdermoving toIN_KITCHENat once, and have the kitchen submit items in course groups - a bigger change than it looks, since the state machine moves fromOrderdown toOrderItem. - How do you split a check between two diners at one table?
OrderItemwould need an owner reference (which diner ordered it), and billing would sum by that owner instead of by the whole order -OrderandTabledon't need to change, since splitting is a read-time concern over data already captured. - What happens if a MenuItem gets 86'd while it's already in three open orders' item
lists? Nothing retroactively -
availableis only checked ataddItem()time, so already-addedOrderItems stay in those orders; only futureaddItem()calls for that item are blocked. - How would a fourth kitchen station (say, a bar) get added? Add a new
Stationenum value and set it on the relevantMenuItems -groupByStation()already partitions by whateverStationvalues exist, so no new code runs, only new data.
Check yourself
Why is addItem() only legal while the order is PLACED, checked inside addItem() itself?