Contracts — Example: Restaurant Ordering System
The Scenario
A restaurant with table service and online ordering. Customers dine in or order delivery. Waitstaff take orders at the table. Kitchen receives orders and marks them complete. The system calculates bills, splits checks, and processes payment. Tips are recorded.
This is a different domain from the library — more real-time, more physical-world interaction, and more complex pricing.
Contract 1: Create Table Order
CONTRACT: CreateTableOrder
ACCEPTS:
- table_number: number — required, must be a valid table in the system (1-30)
- server_id: text — required, must be a valid staff ID for an active server
- party_size: number — required, must be 1-12
RETURNS:
- order:
- order_id: text (unique)
- table_number: number
- server_id: text
- server_name: text
- party_size: number
- opened_at: timestamp
- status: "open"
- items: empty list (no items yet)
- subtotal: 0.00
ERRORS:
- table_number not found → error: "Invalid table number"
- table already has an active order → error: "Table [N] already has an open order
(order_id: [X]). Close or transfer it first."
- server_id not found → error: "Unknown server"
- server is clocked out → error: "Server is not currently clocked in"
- party_size is 0 or negative → error: "Party size must be at least 1"
- party_size exceeds table capacity → error: "Table [N] seats [X]. Party of [Y]
requires a different table."
- database unreachable → error: "System temporarily unavailable"
SIDE EFFECTS:
- Table status changed to "occupied" in the floor plan system
- Order opened event logged with timestamp
Design Notes
Table capacity checking. The contract validates party size against table capacity — a business rule that prevents operational problems (8 people at a 4-person table). This data comes from the floor plan, which is configuration data.
"Table already has an active order" is common. Servers sometimes forget to close a tab. Instead of silently creating a second order, the error forces the server to deal with the existing one.
Contract 2: Add Item to Order
CONTRACT: AddItemToOrder
ACCEPTS:
- order_id: text — required
- menu_item_id: text — required, must be a valid item from the active menu
- quantity: number — required, must be 1-20
- modifications: list of text — optional (e.g., ["no onions", "extra cheese",
"sub gluten-free bun"])
- seat_number: number — optional (for tracking who ordered what within a party)
- special_instructions: text — optional, max 200 characters
RETURNS:
- updated_order:
- order_id: text
- items: list (now includes the new item)
- Each item:
- line_item_id: text (unique per item in the order)
- menu_item_id: text
- item_name: text
- quantity: number
- unit_price: currency
- modifications: list of text
- modification_charges: currency (extra cheese = $1.50, etc.)
- line_total: currency (quantity × (unit_price + modification_charges))
- seat_number: number or null
- special_instructions: text or empty
- status: "ordered"
- subtotal: currency (updated sum of all line_totals)
ERRORS:
- order_id not found → error: "Unknown order"
- order is not open (already closed/paid) → error: "Order is closed. Cannot add items."
- menu_item_id not found → error: "Unknown menu item"
- menu item is unavailable (86'd) → error: "[Item name] is currently unavailable"
- modification is not recognized → error: "Unknown modification: [text].
Available modifications: [list]"
- modification is not applicable to this item → error: "'[modification]' cannot be
applied to [item name]"
- quantity exceeds limit → error: "Maximum quantity per line item is 20"
SIDE EFFECTS:
- Order sent to kitchen display (Transport to kitchen) with new item(s) highlighted
- If item has an allergy flag (e.g., contains nuts), allergy alert included in
kitchen display
- Inventory for ingredients decremented (optional — depends on whether the restaurant
tracks ingredient inventory in real time)
- Item addition logged with server_id and timestamp
Why Modifications Are Complex
Modifications look simple ("no onions") but they create contract complexity:
- Some modifications are free ("no onions" — they're removing something)
- Some modifications have a charge ("extra cheese" = $1.50, "add avocado" = $2.00)
- Some modifications are impossible ("sub gluten-free bun" on a salad)
- Some modifications create allergy implications ("add peanut sauce")
The contract must handle all of these. A vague contract ("accepts modifications: list") leaves all of this to guesswork.
Contract 3: Send Order to Kitchen
CONTRACT: SendToKitchen
ACCEPTS:
- order_id: text — required
- items_to_send: list of line_item_ids — optional. If empty, sends all
items with status "ordered" (not yet sent)
Note on "courses": A server might take the full order upfront but send
appetizers to the kitchen first, entrées later. This contract supports that
by allowing partial sends.
RETURNS:
- kitchen_ticket:
- ticket_id: text
- order_id: text
- table_number: number
- items: list of items being sent
- Each item: name, quantity, modifications, special instructions, seat number
- sent_at: timestamp
- allergy_alerts: list (any items flagged with allergy concerns)
- estimated_prep_time: minutes (calculated from item prep times)
ERRORS:
- order_id not found → error: "Unknown order"
- no items to send (all items already sent or order is empty) → error:
"No unsent items on this order"
- line_item_id not found in order → error: "Item [id] not found on order [id]"
- kitchen is in "overflow" status → warning (not error): "Kitchen is backed up.
Current estimated wait: [X] minutes." (Order is still accepted — this is
informational.)
SIDE EFFECTS:
- Items' status changed from "ordered" to "sent to kitchen"
- Kitchen display updated with new ticket
- Ticket print at appropriate kitchen station (grill items → grill station,
salads → cold station, etc.)
- Estimated wait time sent back to server's device
The Course Problem
Real restaurants have courses. Appetizers go first, then entrées, then dessert. The contract handles this by allowing the server to choose which items to send. But the contract doesn't enforce course ordering — a server could send desserts first. Is that an error?
Decision: No. The contract allows it. The server might have a reason (the customer wants dessert only). Business rules about course ordering are the server's training, not the system's enforcement. This is a deliberate contract design choice — not every rule belongs in the software.
Contract 4: Close Order and Calculate Bill
CONTRACT: CalculateBill
ACCEPTS:
- order_id: text — required
- split_method: one of ["no_split", "equal_split", "by_seat", "custom"]
- If "equal_split": split_count: number (how many ways to split, 2-12)
- If "by_seat": (no additional input — each seat gets their items)
- If "custom": custom_splits: list of {split_label: text, line_item_ids: list}
RETURNS:
- bill:
- order_id: text
- splits: list of:
- split_id: text
- split_label: text ("Check 1", "Seat 3", "Jordan's portion", etc.)
- items: list of items in this split
- subtotal: currency
- tax: currency (calculated from local tax rate)
- total: currency (subtotal + tax)
- order_subtotal: currency (pre-tax sum of all splits)
- order_tax: currency
- order_total: currency
- gratuity_suggestion:
- 15_percent: currency
- 18_percent: currency
- 20_percent: currency
- 25_percent: currency
(calculated on pre-tax subtotal)
ERRORS:
- order_id not found → error: "Unknown order"
- order has no items → error: "Cannot generate bill for empty order"
- order has items with status "sent to kitchen" but not "completed" →
warning: "Kitchen has not completed all items. Generate bill anyway?"
- split_method "by_seat" but some items have no seat assigned → error:
"[N] items have no seat number. Assign seats or use a different split method."
- custom_splits don't cover all items → error: "The following items are not
assigned to any split: [list]"
- custom_splits assign the same item to multiple splits → error: "Item [name]
is assigned to multiple splits"
- database unreachable → error: "System temporarily unavailable"
SIDE EFFECTS:
- Order status changed to "bill generated"
- Bill event logged with split details and timestamp
The Split Check Problem
Check splitting might be the most complex everyday contract. Consider:
- Equal split — simple math, but what about items that cost significantly more? Equitable ≠ equal.
- By seat — requires every item to be assigned to a seat. If the server didn't track seats, this fails.
- Custom — maximum flexibility, but the contract must verify that all items are covered (no orphans) and no item is double-counted.
The contract handles all three approaches with clear errors for each. A weaker contract would just say "accepts split_method: text" and leave all validation to implementation.
Contract 5: Process Payment
CONTRACT: ProcessPayment
ACCEPTS:
- split_id: text — required (pays one split at a time)
- payment_method: one of ["cash", "credit_card", "debit_card", "gift_card"]
- If credit/debit: card_token: text (tokenized card data, never raw card numbers)
- If gift_card: card_number: text, pin: text
- If cash: amount_tendered: currency
- tip_amount: currency — optional, default 0.00
RETURNS:
- payment_receipt:
- payment_id: text
- split_id: text
- amount_charged: currency
- tip_amount: currency
- total_charged: currency (amount + tip)
- payment_method: text
- change_due: currency (only for cash, 0.00 otherwise)
- paid_at: timestamp
ERRORS:
- split_id not found → error: "Unknown split"
- split already paid → error: "This split has already been paid"
- credit card declined → error: "Card declined. Reason: [reason from processor]"
- gift card insufficient balance → error: "Gift card balance is [X].
Split total is [Y]. Remaining [Z] must be paid by another method."
- cash amount_tendered is less than total → error: "Amount tendered ([X])
is less than total ([Y])"
- tip_amount is negative → error: "Tip cannot be negative"
- database unreachable → error: "System temporarily unavailable"
- payment processor unreachable → error: "Payment system temporarily unavailable.
Cash payment available."
SIDE EFFECTS:
- Split marked as "paid"
- If all splits for the order are paid, order status changed to "closed"
- If order is closed, table status changed to "available" in floor plan
- Payment logged for accounting/end-of-day reconciliation
- Tip recorded and attributed to server for tip-out calculations
- If cash: cash drawer amount updated
- Receipt generated for printing or digital delivery
How These Contracts Chain Together
A complete table service flow:
CreateTableOrder(table_5, server_12, party_4)
↓
AddItemToOrder(order_001, "calamari", qty=1)
AddItemToOrder(order_001, "burger", qty=2, mods=["no onions"], seat=1)
AddItemToOrder(order_001, "pasta", qty=1, seat=2)
AddItemToOrder(order_001, "salmon", qty=1, seat=3)
↓
SendToKitchen(order_001, items=["calamari"]) ← Appetizer first
↓
... kitchen prepares and marks complete ...
↓
SendToKitchen(order_001) ← Remaining items (entrées)
↓
... kitchen prepares and marks complete ...
↓
CalculateBill(order_001, split_method="by_seat")
↓
ProcessPayment(split_seat1, credit_card, tip=8.00)
ProcessPayment(split_seat2, cash, amount_tendered=30.00)
ProcessPayment(split_seat3, credit_card, tip=6.00)
ProcessPayment(split_seat4, gift_card, tip=5.00)
↓
Order closed. Table 5 available.
Each step has its own contract. Each step can fail independently with clear error messages. The chain is explicit — no hidden dependencies.
Comparing Library vs. Restaurant Contracts
| Aspect | Library | Restaurant |
|---|---|---|
| Complexity of inputs | Simple (IDs) | Complex (modifications, split methods, multiple payment types) |
| Error case count per contract | 6-9 | 7-10 |
| Side effects crossing modules | Catalog, Finances, Communication | Kitchen display, Floor plan, Accounting |
| Time sensitivity | Relaxed (books are due in 14 days) | High (food gets cold, customers get impatient) |
| Physical-world interaction | Book in/out | Food preparation, cash handling |
| Business rules in contracts | "Can't hold available books" | "Can't split by seat without seat assignments" |
| Partial operations | Partial (hold vs. checkout) | Extensive (courses, split checks, partial payments, gift card remainder) |
The restaurant contracts are more complex because the domain is more complex — but the structure is identical: name, inputs, outputs, errors, side effects.