Object-Oriented Design Interview

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.

TableFREESEATEDORDER OPENBILL PRESENTEDparty seatedfirst item orderedbill requestedpaid and clearedOrder itemORDEREDSENT TO KITCHENCOOKINGREADYSERVEDCANCELLEDsentstation picks it upplatedrunner delivers86'd or sent backfirst item ORDERED opens the table'sorderevery item SERVED or CANCELLEDallows the billThe table cannot be billed while any item is still cooking — the two machines constrain each other, which is why they are drawn together.
One table has many items in flight at once; the table's state is a function of all of them.

Where they synchronise, precisely

Three moments, and the third is different in kind:

  1. Seating creates the order. No order item can exist before its table is occupied.
  2. All items served allows the bill. The bill is closable when every item is SERVED or CANCELLED.
  3. Closing the table is guarded, not triggered. A table cannot move to NEEDS_CLEANING while any item is PREPARING.
Java
public void closeTable(Seating seating) {    boolean allSettled = seating.getOrder().getItems().stream()        .allMatch(i -> i.getStatus() == SERVED || i.getStatus() == CANCELLED);    if (!allSettled) {        throw new ItemsStillInProgressException(seating.getOrder().getId());    }    seating.getTable().changeState(NEEDS_CLEANING);    seating.setEndedAt(clock.now());}

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.

Two patterns, each justified against a direct callEarns its place• Command: an order item as an object• Command gives queueing, undo, priority• Observer: status change fans outLeft out, and say why• Singleton for the restaurant object• Strategy where one pricing rule exists• Factory for a two-field Reservation
Observer beats a direct call here because the kitchen must not know who is listening.

Command, for order items

The mess it removes. Without it, modifying an order is a mutation:

Java
order.getItems().remove(item);                 // cancelleditem.setQuantity(3);                           // changedorder.getItems().add(new OrderItem(dessert));  // added

Three 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:

Java
public interface OrderCommand {    void execute(Order order, KitchenRouter router);    void undo(Order order, KitchenRouter router);    String description();          // for the audit log    Staff issuedBy();}public class CancelItemCommand implements OrderCommand {    private final OrderItem item;    private OrderItemStatus previousStatus;    public void execute(Order order, KitchenRouter router) {        previousStatus = item.getStatus();        if (previousStatus == READY || previousStatus == SERVED) {            throw new CannotCancelException(item.getId(), previousStatus);        }        item.setStatus(CANCELLED);        router.withdrawFromTicket(item);                       // tell the kitchen        if (previousStatus == PREPARING) {            order.recordWastage(item);                         // the food exists; someone pays        }    }    public void undo(Order order, KitchenRouter router) {        item.setStatus(previousStatus);        router.reinstateOnTicket(item);    }}// 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:

Java
public void markReady(OrderItem item) {    item.setStatus(READY);    kitchenDisplay.refresh();               // the kitchen now imports the display    serverDevice.notify(item);              // ... and the server's device    expediterScreen.refresh();              // ... and the pass screen    billingService.recalculate(item);       // ... and billing}

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.

Java
public interface OrderStatusListener {    void onStatusChanged(OrderItemStatusEvent event);}public class OrderItem {    public void changeStatus(OrderItemStatus next) {        OrderItemStatus previous = this.status;        this.status = next;        eventBus.publish(new OrderItemStatusEvent(this, previous, next, clock.now()));    }}

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 changeStatus no 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

CandidateWhy not
Singleton for RestaurantGlobal mutable state; a chain would need several. Create one and inject it
Strategy for pricingPrices are fields on menu items. Add it when promotions arrive, not before
State classes for TableFour states, no per-state behaviour beyond validity. An enum with a transition check is honest
Factory for OrderItemOne 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:

Five follow-ups across four subsystemsRestaurantextensionsSplit bills per itemCourse timingWalk-ins vs bookingsDelivery integrationScaling to a chain
Separating these three features rather than merging them is itself the answer being looked for.
  • 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 OrderItem is assigned to a payer, so Bill becomes several bills over disjoint subsets of items. This is why Bill was modelled with a line list rather than a single total.
  • Partial payment. One bill, several payments, possibly by different methods. The Bill to Payment multiplicity of 1..* 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:

Java
Instant targetServeTime = courseStartTime(courseNumber);for (OrderItem item : itemsInCourse) {    Instant startBy = targetServeTime.minus(item.getMenuItem().prepMinutes());    router.scheduleTicket(item, startBy);          // the kitchen sees a start-by time}

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.