Course Content
Object-Oriented Design Interview
14 sections · 29 lessons
Restaurant Management: lifecycles, Command, Observer and extensions
With orders, tickets, tables and seatings modelled, the capstone's remaining work is behaviour: two lifecycles that have to stay in step, the two patterns that earn their place in this design and the four that do not, and a set of extensions that test every earlier decision — ending where the course hands over to distributed system design.
Order and table lifecycles
Two state machines that interact. Drawing both and marking exactly where they synchronise is the deliverable of this step.
The table lifecycle
FREE → RESERVED → OCCUPIED → NEEDS_CLEANING → FREE.
The state candidates forget is NEEDS_CLEANING. A table whose party has left is not available — it has plates on it. Without that state, the host seats a party at a dirty table, which is a real restaurant failure and a nice detail to have thought of.
The order-item lifecycle
ORDERED → PREPARING → READY → SERVED, with CANCELLED reachable from the first two.
Two decisions are embedded there:
Status lives on the item, not the order. A table's salad is ready while its steak is still cooking. An order-level status cannot express that, and the whole point of the kitchen display is per-item progress.
Cancellation is only possible before READY — and cancelling from PREPARING incurs a cost, because the food exists. That cost is why the design uses Command (patterns applied, below): the cancellation is an auditable event, not a silent mutation.
Where they synchronise, precisely
Three moments, and the third is different in kind:
- Seating creates the order. No order item can exist before its table is occupied.
- All items served allows the bill. The bill is closable when every item is
SERVEDorCANCELLED. - Closing the table is guarded, not triggered. A table cannot move to
NEEDS_CLEANINGwhile any item isPREPARING.
1public void closeTable(Seating seating) {2 boolean allSettled = seating.getOrder().getItems().stream()3 .allMatch(i -> i.getStatus() == SERVED || i.getStatus() == CANCELLED);4 if (!allSettled) {5 throw new ItemsStillInProgressException(seating.getOrder().getId());6 }7 seating.getTable().changeState(NEEDS_CLEANING);8 seating.setEndedAt(clock.now());9}The distinction between a synchronising trigger and a synchronising guard is worth naming out loud. Two state machines usually interact both ways, and candidates who show only triggers have missed half of it.
Patterns applied
Two patterns, each justified against the direct-call alternative. As always, the justification is the scoring part.
Command, for order items
The mess it removes. Without it, modifying an order is a mutation:
order.getItems().remove(item); // cancelleditem.setQuantity(3); // changedorder.getItems().add(new OrderItem(dessert)); // addedThree problems, all of which a restaurant manager will eventually call about:
- No audit trail. "Who cancelled the steak that was already cooking, and when?" is unanswerable.
- No undo. A server cancels the wrong line and there is no way back.
- The kitchen is not told. The list changed; the ticket at the grill did not.
The repair. Every change to an order is an object:
1public interface OrderCommand {2 void execute(Order order, KitchenRouter router);3 void undo(Order order, KitchenRouter router);4 String description(); // for the audit log5 Staff issuedBy();6}78public class CancelItemCommand implements OrderCommand {9 private final OrderItem item;10 private OrderItemStatus previousStatus;1112 public void execute(Order order, KitchenRouter router) {13 previousStatus = item.getStatus();14 if (previousStatus == READY || previousStatus == SERVED) {15 throw new CannotCancelException(item.getId(), previousStatus);16 }17 item.setStatus(CANCELLED);18 router.withdrawFromTicket(item); // tell the kitchen19 if (previousStatus == PREPARING) {20 order.recordWastage(item); // the food exists; someone pays21 }22 }2324 public void undo(Order order, KitchenRouter router) {25 item.setStatus(previousStatus);26 router.reinstateOnTicket(item);27 }28}29// AddItemCommand, ChangeQuantityCommand, AddModifierCommand follow the same shape.Order keeps a list of executed commands, which is the audit trail — no separate logging needed.
The honest justification, and its limit:
"Command earns its place here because the requirements ask for reversible, auditable modifications, and because a change has to reach the kitchen as well as the order. The command list is the audit log for free. If the requirement were only 'add items to an order', this would be over-engineering and I'd write a method."
That second sentence is as important as the first. It is the test from When not to use a pattern applied in the affirmative — and stating the condition under which you would not use the pattern proves the choice was a judgement.
Observer, for status changes
The mess it removes. Without it, marking an item ready means the kitchen knows about everyone:
1public void markReady(OrderItem item) {2 item.setStatus(READY);3 kitchenDisplay.refresh(); // the kitchen now imports the display4 serverDevice.notify(item); // ... and the server's device5 expediterScreen.refresh(); // ... and the pass screen6 billingService.recalculate(item); // ... and billing7}Four dependencies from a kitchen method, and a fifth screen means editing it again. Requirement N2 said the kitchen must not know who cares.
The repair.
1public interface OrderStatusListener {2 void onStatusChanged(OrderItemStatusEvent event);3}45public class OrderItem {6 public void changeStatus(OrderItemStatus next) {7 OrderItemStatus previous = this.status;8 this.status = next;9 eventBus.publish(new OrderItemStatusEvent(this, previous, next, clock.now()));10 }11}The kitchen display, the server's device, the expediter's screen, and billing each subscribe. A new screen is a new subscriber and zero edits.
The two costs, named:
- Control flow becomes indirect. Reading
changeStatusno longer tells you what happens next. Mitigate it by keeping the event set small and documented. - One slow or failing listener must not break a kitchen operation. Catch per listener, and in anything beyond a single process, publish to a queue rather than calling inline — the same caveat as the grocery checkout.
The patterns to leave out
| Candidate | Why not |
|---|---|
Singleton for Restaurant | Global mutable state; a chain would need several. Create one and inject it |
| Strategy for pricing | Prices are fields on menu items. Add it when promotions arrive, not before |
State classes for Table | Four states, no per-state behaviour beyond validity. An enum with a transition check is honest |
Factory for OrderItem | One construction site. A constructor is enough |
Saying "I'm not using State classes for the table — four states with no behaviour of their own, so an enum plus a validated transition is simpler and I'd rather spend the complexity budget on the kitchen" is a strong close. It shows a complexity budget exists and that you are spending it deliberately.
Extensions
1. Split bills and per-item assignment
Three different features, and separating them is the answer:
- Even split. Divide the total by N. Trivial, and rounding must be handled — someone pays the extra rupee, and saying that out loud is a small credibility point.
- By item. Each
OrderItemis assigned to a payer, soBillbecomes several bills over disjoint subsets of items. This is whyBillwas modelled with a line list rather than a single total. - Partial payment. One bill, several payments, possibly by different methods. The
BilltoPaymentmultiplicity of1..*from Finding the objects already covers this.
The design point: if Bill holds a total and a payment, all three are painful. If it holds lines and a list of payments, all three are configuration. That multiplicity was a decision, and this extension is where it pays.
2. Course timing
The hardest extension, and a genuinely interesting scheduling problem. A table's mains should arrive together and after the starters, but a steak takes fourteen minutes and a salad takes two.
Working backwards from the target serve time:
1Instant targetServeTime = courseStartTime(courseNumber);2for (OrderItem item : itemsInCourse) {3 Instant startBy = targetServeTime.minus(item.getMenuItem().prepMinutes());4 router.scheduleTicket(item, startBy); // the kitchen sees a start-by time5}Two honest caveats to raise:
- Prep times are estimates, and a busy grill runs behind. The system suggests; the expediter decides. A design that assumes its estimates are truth will produce cold food.
- This makes the kitchen ticket a scheduled item rather than a queued one, which changes the kitchen display from "oldest first" to "start-by soonest first". That is a one-line change to a comparator, which is a good sign the model was right.
3. Walk-ins against reservations
The policy question: a walk-in party of two arrives, one table is free, and a reservation for four is due in twenty minutes on that table.
There is no correct answer, only a policy — which is the honest thing to say:
- Reservations always win. Predictable, and the table sits empty for twenty minutes.
- Seat the walk-in if they will likely be done in time. Uses the table, and risks making the reservation wait, which is the worse failure for a restaurant's reputation.
- Seat the walk-in at a table unsuitable for the reservation. Best of both, and needs the host to be reasoning about the whole floor rather than one table.
Recommend the third with a fallback to the first, and note the design consequence: table assignment must be a strategy over the whole floor, not a first-fit over one table. That is the same insight as the elevator dispatcher in Section 8 — the decision belongs to something that sees everything, not to the resource itself.
4. Delivery integration
Delivery orders are orders with no Seating and a delivery address. If Order requires a Seating, this extension hurts; if Seating is optional and an order has an OrderChannel (dine-in, takeaway, delivery), it is a field.
Test your own model here: could you take a takeaway order without a table? If not, the coupling between Order and Table was too tight, and that is worth noticing out loud.
5. Scaling to a chain
The menu becomes shared and per-location priced — the Product / InventoryItem split from the grocery objects again. Tables, seatings, orders, and tickets stay strictly per-location, and that is a useful thing to say: a restaurant's operational data does not want to be centralised, because the kitchen must keep working when the network to head office is down. Reporting is a separate read path that tolerates delay.
That answer bridges into System Design Interview and is the right way to close this course: the class model you have built is the inside of one service, and the boundaries you drew — kitchen versus order, location versus chain — are where services would be split later.