Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,102visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsMachine Coding Exercises
✓ FreeAdvanced· 7 min read

Multi-Pattern Problem: Design a Food Ordering Class Model

Combining State (order lifecycle) + Observer (customer/restaurant notification) — a smaller-scope companion to the Ride Booking multi-pattern exercise, with a direct focus on testability.

Published September 23, 2026


Multi-Pattern Problem: Design a Food Ordering Class Model

A deliberately smaller two-pattern combination (State + Observer, no Strategy this time) — worth doing specifically to practice the same justification discipline as the Ride Booking exercise, but without pricing/matching complexity distracting from the core two-pattern interaction.

Core classes

interface OrderState { OrderState advance(FoodOrder order); }
class PlacedState implements OrderState { public OrderState advance(FoodOrder order) { return new ConfirmedState(); } }
class ConfirmedState implements OrderState { public OrderState advance(FoodOrder order) { return new PreparingState(); } }
class PreparingState implements OrderState { public OrderState advance(FoodOrder order) { return new OutForDeliveryState(); } }
class OutForDeliveryState implements OrderState { public OrderState advance(FoodOrder order) { return new DeliveredState(); } }
class DeliveredState implements OrderState { public OrderState advance(FoodOrder order) { return this; } }

interface OrderObserver { void onStateChanged(FoodOrder order, OrderState newState); }
class CustomerNotificationObserver implements OrderObserver { public void onStateChanged(FoodOrder order, OrderState state) { /* push notification */ } }
class RestaurantDashboardObserver implements OrderObserver { public void onStateChanged(FoodOrder order, OrderState state) { /* update kitchen display */ } }

class FoodOrder {
    private OrderState state = new PlacedState();
    private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();

    void advanceState() {
        state = state.advance(this);
        observers.forEach(o -> o.onStateChanged(this, state));
    }
}

This is a near-identical shape to Ride Booking's State+Observer half (and to Restaurant Management System's own order-state flow) — worth explicitly recognizing the repetition rather than re-deriving it as if novel, which is itself a strong signal: pattern recognition includes noticing "I've solved this exact combination before," not just recalling individual pattern definitions.

Advertisement

Testability: the specific angle this exercise asks for

@Test
void placedStateAdvancesTo Confirmed() {
    OrderState state = new PlacedState();
    OrderState next = state.advance(new FoodOrder()); // no mocking needed — advance() only depends on its own logic
    assertInstanceOf(ConfirmedState.class, next);
}

@Test
void advancingOrderNotifiesAllObservers() {
    FoodOrder order = new FoodOrder();
    OrderObserver mockObserver = mock(OrderObserver.class);
    order.addObserver(mockObserver);
    order.advanceState();
    verify(mockObserver).onStateChanged(eq(order), any(ConfirmedState.class));
}

Each OrderState implementation is testable in complete isolation — PlacedState.advance() needs nothing beyond a FoodOrder reference (which could be a bare, minimally-constructed instance, not a fully wired-up object graph) to verify it transitions correctly. Separately, FoodOrder.advanceState()'s notification behavior is testable with a mock OrderObserver, verifying the fact that observers get called on every transition without needing a real CustomerNotificationObserver (which would require mocking a push-notification service) at all. This isolation — testing state-transition logic and notification-firing behavior as two entirely separate concerns — is a direct, concrete payoff of State+Observer's decomposition, not just an abstract design-quality claim.

Adding cancellation and payment without breaking the model

The linear advance() chain can't express two things every real ordering system needs: cancellation (allowed early, refused late) and payment failure (the order never progresses). Instead of adding if checks around the state classes, give the state interface events, each with a sensible default:

interface OrderState {
    String name();
    default OrderState onPaymentConfirmed(FoodOrder o) { throw illegal("payment confirmed"); }
    default OrderState onPaymentFailed(FoodOrder o)    { throw illegal("payment failed"); }
    default OrderState onRestaurantAccepted(FoodOrder o) { throw illegal("restaurant accepted"); }
    default OrderState onPickedUp(FoodOrder o)         { throw illegal("picked up"); }
    default OrderState onDelivered(FoodOrder o)        { throw illegal("delivered"); }
    default OrderState onCancelRequested(FoodOrder o)  { throw illegal("cancel"); }

    private IllegalStateException illegal(String event) {
        return new IllegalStateException("Event '" + event + "' not allowed in state " + name());
    }
}

final class AwaitingPayment implements OrderState {
    public String name() { return "AWAITING_PAYMENT"; }
    public OrderState onPaymentConfirmed(FoodOrder o) { return new Placed(); }
    public OrderState onPaymentFailed(FoodOrder o)    { return new Cancelled("payment failed"); }
    public OrderState onCancelRequested(FoodOrder o)  { return new Cancelled("customer cancelled"); }
}

final class Placed implements OrderState {
    public String name() { return "PLACED"; }
    public OrderState onRestaurantAccepted(FoodOrder o) { return new Preparing(); }
    public OrderState onCancelRequested(FoodOrder o) {
        o.refundInFull();                                     // nothing has been cooked yet
        return new Cancelled("customer cancelled");
    }
}

final class Preparing implements OrderState {
    public String name() { return "PREPARING"; }
    public OrderState onPickedUp(FoodOrder o) { return new OutForDelivery(); }
    public OrderState onCancelRequested(FoodOrder o) {
        throw new IllegalStateException("Order is being prepared; contact support to cancel");
    }
}

record Cancelled(String reason) implements OrderState {
    public String name() { return "CANCELLED"; }                // terminal: every event is illegal
}

And FoodOrder applies events in one place, notifying observers only after a successful transition:

final class FoodOrder {
    private OrderState state = new AwaitingPayment();
    private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();

    synchronized void apply(Function<OrderState, OrderState> event) {
        OrderState next = event.apply(state);          // throws if the event isn't allowed here
        OrderState previous = state;
        state = next;
        observers.forEach(obs -> obs.onStateChanged(this, previous, next));
    }
}

order.apply(s -> s.onCancelRequested(order));

What this buys you:

  • Each state lists only the events it accepts. Everything else fails with a clear message, so an illegal transition (delivering a cancelled order) can't happen silently.
  • Cancellation rules live where they belong: free in PLACED, refused in PREPARING, impossible once delivered.
  • Terminal states (Cancelled, Delivered) override nothing, so they accept no events.
  • Observers get both the old and new state, so the notification observer can phrase messages correctly ("Your order was cancelled before preparation").

Testing the combination

Each pattern gets its own focused test, which was the point of the exercise:

@Test void cancellingWhilePreparingIsRefused() {
    FoodOrder order = orderIn(new Preparing());
    assertThrows(IllegalStateException.class, () -> order.apply(s -> s.onCancelRequested(order)));
}

@Test void observersAreNotifiedOnlyForSuccessfulTransitions() {
    FoodOrder order = orderIn(new Preparing());
    RecordingObserver rec = new RecordingObserver();
    order.addObserver(rec);
    assertThrows(IllegalStateException.class, () -> order.apply(s -> s.onCancelRequested(order)));
    assertTrue(rec.events().isEmpty());
}

@Test void pricingStrategyIsIndependentOfState() {
    assertEquals(Money.of(450), new SurgePricing(1.5).price(orderWithSubtotal(300)));
}

State tests don't need observers, observer tests use a trivial recording observer, and pricing tests need no orders in any particular state. When a bug appears, the failing test points to one pattern.

Follow-up questions this topic invites — and their answers

Q: What would testing look like WITHOUT this decomposition — say, if all logic lived in one FoodOrder.advance() method with a switch statement? A: Every test would need to construct a fully-wired FoodOrder AND stub/mock whatever notification mechanism was hard-coded inside that one method — testing 'does PLACED correctly transition to CONFIRMED' would be entangled with testing 'does a notification get sent,' making it impossible to verify one without the machinery for the other, exactly the coupling this design avoids.

Q: Why do CustomerNotificationObserver and RestaurantDashboardObserver both exist as separate classes rather than one CombinedObserver handling both concerns? A: Single Responsibility again — customer-facing push notifications and an internal kitchen dashboard update are genuinely different concerns with different failure modes and different owners; combining them would mean a kitchen-dashboard-update bug risking breaking customer notifications too, an unnecessary coupling this separation avoids.

Q: Is CopyOnWriteArrayList the right choice for FoodOrder's observers list here, same as elsewhere in this course? A: Yes, for the same reason as Design a Notification/Observer-Based Pub-Sub — observers are registered rarely (once, typically at order creation) and iterated frequently (on every state change), exactly CopyOnWriteArrayList's target profile.

Q: How would you extend this to support order cancellation, which doesn't fit the linear advance() progression? A: A cancellation likely needs to be modeled as a transition available from MULTIPLE states (Placed, Confirmed, Preparing can all potentially be cancelled; OutForDelivery/Delivered likely can't) — this would mean adding a separate cancel() method to the OrderState interface (not just advance()), with each state implementation deciding whether cancellation is even valid from that state, a natural extension of the same interface-per-state structure.

Q: Why give the state interface one method per event instead of a single advance()? A: Real workflows branch: payment can succeed or fail, and cancellation is sometimes allowed. Event methods let each state accept only the events valid for it, with default implementations rejecting the rest. That makes illegal transitions explicit errors, rather than hidden bugs.

Q: Why notify observers only after the state has changed successfully? A: If observers ran first, and the transition then failed, customers could be told about changes that never happened. Computing the next state first, assigning it, then notifying, keeps notifications consistent with the actual order state.

Q: Where should refund logic live when an order is cancelled? A: The decision belongs to the state transition (only certain states refund, and possibly partially). The actual refund is best delegated to a payment service, triggered through the order or through an observer reacting to the cancellation, so the state classes stay free of payment-provider details.

Advertisement

Previous

Multi-Pattern Problem: Design a Ride Booking Class Model

Next

Design an Authentication/Authorization Module

AI Tutor

Lesson: Multi-Pattern Problem: Design a Food Ordering Class Model

Quick actions

AI responses can be inaccurate. Verify critical information.