Code a restaurant ordering system
Open in the interactive app: practice, quiz, mock interview →In one minute
- A restaurant system is three small systems sharing one object: the check. The front of house seats guests and adds items to it, the kitchen cooks what was sent from it, and the cashier turns it into money.
- The key separation is between adding an item and firing it. Servers build an order at the table and send it to the kitchen in one step, so a guest can change their mind for free until the kitchen sees it, and courses can be held back and fired later.
- Each order line is a small state machine (pending, fired, cooking, done, void). Who may void a line and what it costs depends on that state, which is where waste and fraud live.
- Fired lines are routed to stations (grill, fryer, bar), each a FIFO queue that cooks pull from. That is a producer-consumer queue with a screen on it.
- Money is integer cents with one explicit rounding rule for tax and for splitting. The problem shapes are ordering (first fired, first cooked), contention (two servers on one check, two hosts on one table) and multi-step consistency (a check is not closed until every line is accounted for).
Requirements
Functional:
- Hosts seat parties at tables and keep a waitlist when the room is full.
- Servers open a check for a table, add items with quantities and notes, and fire them to the kitchen; later items can be added and fired separately.
- Each menu item belongs to a station. Fired lines appear on that station's screen in the order they were fired; a cook starts a line, then bumps it as done.
- A line can be voided before cooking starts. After that, only a manager can comp it, and it is recorded as waste.
- Closing a check computes subtotal, tax and the total, splits it evenly or by seat, takes payment and frees the table.
Non-functional:
- The kitchen never loses a fired ticket, even if the network to the cloud is down; the restaurant must keep running on its local network.
- Totals are reproducible to the cent from the lines and the tax rule.
- Firing reaches the kitchen screen in well under a second; cooks notice a slow screen immediately.
Out of scope: inventory deduction per recipe (see Design Inventory Management System), online ordering and delivery dispatch (see Design Food Delivery Service), payroll and loyalty points.
Entities and relationships
- MenuItem: id, name, price in cents, station and allowed modifiers. Treat it as a versioned value: a price change makes a new version, and lines keep the version they were ordered at.
- Table: id, capacity and state (free, seated, needs cleaning). A WaitlistEntry carries party size and arrival time.
- Check: id, table, guest count, status (open, closed) and its lines. It is the aggregate root for ordering: every change to a line goes through its check.
- OrderLine: id, menu item version, quantity, seat number, course, notes and status. It is the unit the kitchen works on.
- Station: a name and a FIFO queue of fired line ids. A KitchenDisplay observes one or more stations.
- Payment: amount, method and reference, many per check for split bills.
- Restaurant: the façade the server tablets, host stand and kitchen screens call. Policies hang off it: TaxPolicy, SeatingStrategy and SplitPolicy.
Relationships: a check is composed of its lines (a line never exists without a check). A line references a menu item version and, once fired, sits in exactly one station queue. A table has at most one open check. Payments belong to a check.
The modelling decision that pays for itself is line, not check, as the unit of kitchen work. A check with a steak and a beer goes to two stations and finishes at two times. If the kitchen tracked whole checks, the bar would wait for the grill.
Class diagram
classDiagram
class Restaurant {
-checks: Map~CheckId, Check~
-stations: Map~Name, Station~
-tables: Map~TableId, Table~
+openCheck(id, table, guests)
+add(checkId, item, qty) LineId
+fire(checkId) int
+start(station) LineId
+done(lineId)
+void(lineId)
+close(checkId, taxBp, ways) Money[]
}
class Check {
+id: CheckId
+table: TableId
+guests: int
+status: CheckStatus
-lines: OrderLine[]
}
class OrderLine {
+id: LineId
+item: MenuItem
+qty: int
+seat: int
+status: LineStatus
}
class MenuItem {
+id: string
+price: Money
+station: Name
+version: int
}
class Station {
+name: Name
-queue: LineId[]
}
class KitchenDisplay {
<<interface>>
+lineFired(station, line)
}
class TaxPolicy {
<<interface>>
+tax(subtotal) Money
}
Restaurant "1" *-- "many" Check
Check "1" *-- "many" OrderLine
OrderLine --> MenuItem
Restaurant "1" *-- "many" Station
Station o-- OrderLine
Restaurant --> KitchenDisplay
Restaurant --> TaxPolicyKey flows
Firing is the moment the order becomes the kitchen's problem. Everything before it is free to change; everything after it costs food.
sequenceDiagram
participant S as ServerTablet
participant R as Restaurant
participant G as GrillStation
participant B as BarStation
participant K as GrillScreen
S->>R: add(c1, burger, 2) and add(c1, soda, 1)
R-->>S: L1, L2 pending
S->>R: fire(c1)
R->>R: lock check c1, mark L1 and L2 fired
R->>G: enqueue L1
R->>B: enqueue L2
R-->>S: 2 lines fired
R->>K: lineFired(grill, L1)
K->>R: start(grill)
R-->>K: L1 cooking
K->>R: done(L1)
S->>R: close(c1, 825 bp, 3 ways)
R->>R: all lines done, subtotal 2700, tax 223
R-->>S: [975, 974, 974]Design patterns used and why
- Command for order lines once fired. A fired line is a request the kitchen executes later, possibly on a different device, and it can be voided (cancelled) before execution. A command object with an id is queueable, loggable and replayable after a screen reboot. See Command.
- State for the line lifecycle.
voidis legal in pending and fired, illegal in cooking and done;doneis legal only from cooking. A transition table makes these rules testable and visible to the business. - Observer for kitchen screens and printers. Stations publish "line fired", "line voided" and "line done"; screens, the expo printer and the server's tablet subscribe. Publish after the state change commits, so a slow ticket printer cannot hold the lock.
- Producer-consumer for stations: servers produce, cooks consume, and the queue absorbs the Friday 19:30 burst. See Producer Consumer.
- Strategy for tax, seating and splitting. Tax rules differ by city and by dine-in versus takeaway; seating may be smallest-fitting-table or server-rotation; splits may be even, by seat or by item.
- Composite for combos and modifiers. "Burger meal" contains a burger, fries and a drink that go to three stations; a composite menu item expands into three lines when fired while the bill shows one price. See Composite.
What not to do: a class per dish. Dishes differ in data (price, station, modifiers), not behaviour.
Concurrency and edge cases
- Two servers, one check. A server and a bartender both add to table 12's check. Make the check the unit of locking:
add,fire,voidandclosetake the check's lock, or use a version number on the check and reject stale writes from tablets. - Double fire. A tablet on bad Wi-Fi retries
fire. Firing only moves pending lines, so a retry sends nothing new; make that idempotence explicit, because duplicate tickets mean duplicate food. - Void races start. The server voids a line at the moment the cook taps start. Whichever transition takes the line lock first wins: either the line is voided and never appears as cooking, or it is cooking and the void fails, and the server must ask a manager to comp it.
- Two hosts, one table. Seating is select-and-claim under one lock, the same shape as the last spot in Design Parking Lot.
- Closing with food in flight. Closing a check with lines still cooking either bills for food not yet served or loses it. Refuse to close while any line is pending, fired or cooking; voided lines are excluded.
- Rounding. Tax is computed once on the check subtotal with half-up rounding, not per line, or a ten-line check can be off by several cents from what the receipt rules require. An even split gives the leftover cents to the first payers so the parts always sum to the total.
- Menu changes mid-service. Lines keep the menu item version they were ordered at.
Down the stack
Layer 1The contract
add returns a new line id and the line is pending; it fails on a closed or unknown check, an unknown item or a quantity below one. fire(check) moves every pending line of that check to fired and appends it to its station queue in line order, returning how many moved; calling it again with nothing pending returns zero and changes nothing. start(station) returns the oldest fired line at that station and marks it cooking, or nothing. close(check, taxBp, ways) succeeds only when every non-void line is done, returns ways amounts that differ by at most one cent and sum to subtotal + round_half_up(subtotal * taxBp / 10000), and frees the table.
Layer 2The mechanism
Each station queue is a FIFO of line ids (a deque in Python, a slice in Go). Voiding a fired line removes it from its queue, an O(n) scan that is fine because a station rarely holds more than a few dozen lines; a linked hash set makes it O(1) if you need it. Lines live in a hash map by id so screens can address them directly. Real restaurant systems run a local server in the back office and keep tablets, screens and printers on the restaurant LAN, syncing to the cloud when they can, because a dropped internet connection on a Friday night must not stop the kitchen. Receipt and kitchen printers are commonly driven with the ESC/POS command set, a byte stream of text and control codes (cut paper, select font) sent over USB, serial or a raw TCP socket, often on port 9100.
Layer 3The machine
The data is tiny: a busy restaurant fires perhaps a few hundred lines an hour, and a line is under 200 bytes. A mutex at about 20 ns and a SQLite or Postgres commit at 0.1 to 1 ms are invisible next to the physical latencies: a Wi-Fi round trip on a crowded 2.4 GHz band can take tens of milliseconds and occasionally seconds, and a thermal printer prints a ticket in a second or two. The failure that matters is not speed but loss: a tablet that shows "fired" before the local server has durably stored the lines will, on a dropped packet, leave a table waiting forty minutes for food nobody is cooking. So the tablet treats fire as done only after the server acknowledges a committed write, and retries with the same check and line ids otherwise.
Why it's built this way
- Guests change their minds while ordering, so adding and firing are separate steps, and only fired lines cost the kitchen anything.
- One check feeds several stations that finish at different times, so the unit of kitchen work is the line, routed by station.
- Cooks work in arrival order under pressure, so each station is a FIFO queue, and every change to it is visible on a screen.
- Food that has been started cannot be un-cooked, so void is legal only before cooking, and later corrections are a separate, audited comp.
- The internet fails during service, so the system of record for the night runs on the local network, and the cloud is a sync target.
Extensibility
- Courses: add a course number to lines and let
fire(check, course)fire one course at a time. Mains wait until the server fires them after starters are cleared. - Split by seat: lines already carry a seat number; a
BySeatSplitpolicy sums each seat's lines, allocates tax proportionally and gives the remainder cents to the largest seat. - Recipe-based stock: on
done, publish an event that an inventory service consumes to decrement ingredients. Keep it asynchronous so a slow inventory call never blocks the bump. - Service charge for large parties: a pricing step between subtotal and tax, keyed on guest count.
Variant: online food delivery order
Here the order is not a check opened at a table. It is one record shared by three parties who never stand in the same room: the customer, the restaurant and a courier. The question changes from "what does the kitchen cook next" to "who is allowed to make which move, and when".
Requirements shift. One order belongs to one restaurant and carries a subtotal and a delivery fee. The restaurant accepts or rejects it, then marks it ready. A dispatcher assigns a courier after acceptance, and the courier picks up and delivers. Status reads vastly outnumber transitions, about six per order.
Data model. The order is the aggregate root, with status placed, accepted, ready, picked up and delivered, plus the endings rejected and cancelled. Customer, restaurant and courier are held by id only, so the record survives profile changes. Role is derived per order (you are the customer if you placed it, the courier if you are assigned) and is never a field on the user, because one person can be a customer on Monday and a courier on Tuesday.
Mechanism: a transition table. Replace the per-line state checks with rows of (state, action, role) -> next state, refund rule. Every rule becomes readable and testable data. A courier app bug cannot mark someone else's order delivered, because the role lookup fails before any state changes. Each transition appends an immutable history entry with a timestamp.
Money follows state. A cancel before acceptance refunds everything. After acceptance the customer gets the delivery fee back but pays for food the kitchen has started. The refund is computed from the state the order was in when it ended, in integer cents, so subtotal + fee - refunded always explains the charge.
Time is part of the model. An order the restaurant never accepts must expire on its own. Use an injected clock and an idempotent sweep that re-checks status under the order's lock, not a sleep inside a request handler.
Races. Cancel against accept, and expiry against a late accept, are settled by one lock or a versioned conditional update on the order: the first wins and the loser sees the new state and fails as invalid. Courier assignment checks and writes the "one active order per courier" map together. Reassigning before pickup releases the old courier in the same step. A retried "deliver" returns invalid, or the original success if an idempotency key is used.
The shape is multi-step consistency across parties, the same as a pull request with an author, reviewer and CI.
Leadership lens
- Build first: the check and line state machine with fire and station queues, plus offline operation on the local network. A mis-rounded tax line is a refund; a lost ticket is a table that walks out.
- Ask in review: what is the unit of locking; is fire idempotent; what happens to the kitchen when the internet drops; who may comp a cooking line and where is that logged.
- Waste and comps are where money disappears. Every void and comp records who, when, which line and in what state, and the manager report reads straight from that log.
- Ownership: the hardware integration (printers, screens, payment terminals) is a separate team with its own simulator, so the ordering core can be tested without a kitchen.
Check yourself
Why separate adding a line from firing it?
Show answer
Until a line is fired it costs nothing, so the guest and server can change it freely, and courses can be held back. Firing is the commitment point where the kitchen starts using food and time.
Why is the order line, not the check, the unit of kitchen work?
Show answer
A check usually spans several stations that finish at different times. Routing lines lets each station work independently and lets the bar serve drinks without waiting for the grill.
A server voids a line just as the cook taps start. What should happen?
Show answer
The two transitions are serialized on the line. If start wins, the void fails and a manager must comp it; if the void wins, the line disappears from the queue and start returns the next line.
Why compute tax once on the subtotal rather than per line?
Show answer
Rounding each line separately accumulates error of up to half a cent per line, so the receipt total drifts from the tax rule. One rounding on the subtotal is reproducible and matches the receipt.
Watch
- Command Pattern, Design Patterns ep 7 (Christopher Okhravi): the pattern behind fired order lines that can be queued and voided.
- State Pattern, Design Patterns ep 17 (Christopher Okhravi): modelling the line lifecycle as explicit states.
- Object Oriented Design for a Restaurant Reservation System in C#, Part 1 (Clean Code Engineer): a walkthrough of the front-of-house side, tables and reservations.
Free resources
- Point of sale (Wikipedia): background on restaurant POS systems, including kitchen screens and offline operation.
- Brigade de cuisine (Wikipedia): the station structure of a professional kitchen that the station queues mirror.
- Command pattern (Refactoring.Guru): requests as objects that can be queued, logged and cancelled.
- Observer pattern (Refactoring.Guru): how screens and printers subscribe to station events.
- Aggregate (Martin Fowler): why the check is the consistency boundary for its lines.
- ESC/P (Wikipedia): background on the Epson printer control language family that receipt and kitchen printers build on.