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
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.
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.
@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.
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.