Restaurant/Menu/Order/Table/Kitchen classes, order status flow as State pattern, and separating dine-in from takeout/delivery without duplicating logic.
Published September 23, 2026
class Menu { List<MenuItem> items; }
class MenuItem { String name; double price; boolean available; }
class Table { int number; int capacity; TableStatus status; } // dine-in only
class Order {
List<OrderLine> lines;
OrderType type; // DINE_IN, TAKEOUT, DELIVERY
Table table; // null for takeout/delivery
OrderStatus status;
}
interface OrderState {
OrderState advance(Order order); // returns the NEXT state — the transition logic lives per-state
}
class PlacedState implements OrderState {
public OrderState advance(Order order) { notifyKitchen(order); return new PreparingState(); }
}
class PreparingState implements OrderState {
public OrderState advance(Order order) { return new ReadyState(); }
}
class ReadyState implements OrderState {
public OrderState advance(Order order) {
return order.type == OrderType.DINE_IN ? new ServedState() : new AwaitingPickupOrDeliveryState();
}
}
The placed → preparing → ready → served flow is a direct State pattern application (see State Pattern) — each state knows only its own valid next transition, and ReadyState's branch on order.type is the one place order-type-specific flow divergence needs to be expressed, not scattered across the whole class.
Rather than two entirely separate class hierarchies (DineInOrder vs TakeoutOrder), a single Order class with an OrderType field and one branch point (in ReadyState, shown above) keeps the shared 90% of order-handling logic (placement, kitchen notification, line items, pricing) unified, while only the genuinely divergent final step (served to a table vs handed off for pickup/delivery) differs. This is a judgment call worth naming explicitly: full separation into distinct class hierarchies would be over-engineering for a difference this localized — the State pattern's per-state branch is a proportionate amount of structure for one point of real divergence, not the whole flow.
The state classes above always move forward. A real order also gets cancelled, and cancellation is only allowed at certain points: free before the kitchen starts, and needing a manager afterwards. Encoding the allowed transitions in one place makes invalid ones impossible, rather than relying on every caller to check:
enum OrderStatus {
PLACED, PREPARING, READY, SERVED, AWAITING_PICKUP, OUT_FOR_DELIVERY, COMPLETED, CANCELLED;
private static final Map<OrderStatus, Set<OrderStatus>> ALLOWED = Map.of(
PLACED, Set.of(PREPARING, CANCELLED),
PREPARING, Set.of(READY, CANCELLED), // cancelling now wastes food: needs approval
READY, Set.of(SERVED, AWAITING_PICKUP, OUT_FOR_DELIVERY),
SERVED, Set.of(COMPLETED),
AWAITING_PICKUP, Set.of(COMPLETED),
OUT_FOR_DELIVERY, Set.of(COMPLETED),
COMPLETED, Set.of(),
CANCELLED, Set.of());
boolean canMoveTo(OrderStatus next) { return ALLOWED.get(this).contains(next); }
}
class Order {
private OrderStatus status = OrderStatus.PLACED;
private final List<OrderEvent> history = new ArrayList<>();
void moveTo(OrderStatus next, String actor) {
if (!status.canMoveTo(next))
throw new IllegalStateException("Cannot move order from " + status + " to " + next);
history.add(new OrderEvent(status, next, actor, Instant.now()));
status = next;
}
}
This transition-table form is a lighter alternative to one class per state. Use it when states carry little behaviour of their own. Switch to full State-pattern classes when each state has substantial, different behaviour, for example PreparingState reacting to "item out of stock" by notifying the waiter and proposing substitutes. Either way, the history list gives you an audit trail, and makes answering "why was this order late?" possible.
A kitchen isn't one queue. Grill, fryer, cold station and bar prepare different lines of the same order in parallel, and the order is READY only when all of its lines are:
enum Station { GRILL, FRYER, COLD, BAR }
class MenuItem { String name; Money price; Station station; Duration prepTime; boolean available; }
class OrderLine {
final MenuItem item; final int qty; boolean done;
OrderLine(MenuItem item, int qty) { this.item = item; this.qty = qty; }
}
class Kitchen {
private final Map<Station, Queue<OrderLine>> queues = new EnumMap<>(Station.class);
private final Map<OrderLine, Order> owner = new HashMap<>();
Kitchen() { for (Station s : Station.values()) queues.put(s, new ArrayDeque<>()); }
void accept(Order order) {
for (OrderLine line : order.lines()) {
queues.get(line.item.station).add(line); // fan out to stations
owner.put(line, order);
}
order.moveTo(OrderStatus.PREPARING, "kitchen");
}
void markDone(OrderLine line) {
line.done = true;
Order order = owner.remove(line);
if (order.lines().stream().allMatch(l -> l.done)) // fan in: all stations finished
order.moveTo(OrderStatus.READY, "kitchen");
}
}
This fan-out/fan-in structure is the heart of a kitchen display system. Extensions fit naturally: order the station queues by priority (delivery orders with a waiting rider first), or estimate readiness time from the longest prepTime among an order's lines.
Keep billing separate from the order. A Bill is computed from order lines plus a tax and service-charge policy (a Strategy, since it varies by region and order type). Splitting a bill is a list of BillShares whose amounts must add up exactly to the total. Use a money type in the smallest currency unit (paise, cents), and assign rounding remainders deterministically, so three-way splits don't lose or invent a paisa.
Q: Why does Table exist as a separate class rather than just a table number field on Order? A: Table has its own independent state (capacity, current occupancy status) that needs tracking regardless of any specific order — a restaurant needs to know which tables are free for seating NEW parties, which is a Table-level concern that exists before, during, and after any single order's lifecycle, not something that belongs embedded in Order.
Q: How would delivery add a DeliveryPartner assignment step to this flow, connecting to Food Delivery Order Matching? A: AwaitingPickupOrDeliveryState's advance() for a DELIVERY-type order would trigger a matching call (see Food Delivery Order Matching's MatchingEngine) before transitioning further — this is exactly the LLD-to-HLD connection point that class design worth naming: the class model provides the hook, the actual matching algorithm is a separate concern layered on top.
Q: What happens if the kitchen marks an order ready but a dine-in customer has already left (a real edge case)? A: Worth naming as a scenario requiring an explicit state or handling path (e.g. a 'CancelledAfterReady' resolution, or holding the prepared order for a return), rather than assuming the happy-path transition sequence always completes — a strong interview answer names edge cases like this explicitly even without fully designing the resolution.
Q: Should MenuItem availability (available: boolean) be checked at order-placement time or continuously? A: At minimum, checked at placement time (rejecting an order line for an unavailable item) — a more robust design would also handle an item becoming unavailable mid-preparation (the kitchen discovers it's out of an ingredient), which argues for treating availability changes as another event PlacedState/PreparingState might need to react to, not just a static field checked once.
Q: Transition table or State-pattern classes: which should I use in an interview? A: Start with a transition table (an enum plus allowed next states) when states differ mainly in which moves are legal. It's compact and easy to test exhaustively. Move to State classes when individual states need significant behaviour, and say explicitly why you're making that trade.
Q: How does the kitchen know when a multi-station order is ready? A: Each order line is routed to its station's queue, and marked done independently. After each completion, check whether all of that order's lines are done. The last one moves the order to READY. That's a fan-out/fan-in pattern.
Q: Why use integer money amounts instead of double for bills?
A: Binary floating point can't represent most decimal fractions exactly, so sums and splits drift (0.1 + 0.2 ≠ 0.3). Store amounts as integer minor units (or BigDecimal with explicit rounding) and allocate split remainders deterministically.