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:

  1. Some modifications are free ("no onions" — they're removing something)
  2. Some modifications have a charge ("extra cheese" = $1.50, "add avocado" = $2.00)
  3. Some modifications are impossible ("sub gluten-free bun" on a salad)
  4. 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

AspectLibraryRestaurant
Complexity of inputsSimple (IDs)Complex (modifications, split methods, multiple payment types)
Error case count per contract6-97-10
Side effects crossing modulesCatalog, Finances, CommunicationKitchen display, Floor plan, Accounting
Time sensitivityRelaxed (books are due in 14 days)High (food gets cold, customers get impatient)
Physical-world interactionBook in/outFood preparation, cash handling
Business rules in contracts"Can't hold available books""Can't split by seat without seat assignments"
Partial operationsPartial (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.