Combining Strategy (matching, pricing) + State (ride lifecycle) + Observer (real-time updates) in one coherent design, and the discipline of justifying every pattern rather than pattern-stacking for its own sake.
Published September 23, 2026
Every pattern lesson so far has isolated one pattern per exercise. Real interview prompts rarely do — this one deliberately combines three, and the actual skill being tested is knowing why each one earns its place, not just being able to name them.
class Ride {
Rider rider;
Driver driver;
RideState state; // STATE pattern
MatchingStrategy matcher; // STRATEGY pattern
PricingStrategy pricer; // STRATEGY pattern
List<RideObserver> observers = new CopyOnWriteArrayList<>(); // OBSERVER pattern
}
interface MatchingStrategy { Optional<Driver> findDriver(Ride ride, List<Driver> available); }
interface PricingStrategy { double calculateFare(Ride ride); }
Both reuse the exact same reasoning as Food Delivery Order Matching and Design a Ride-Sharing System's matching tradeoffs — which driver gets matched, and how fare is calculated, are independently swappable algorithms with zero reason to be hard-coded into Ride itself.
interface RideState { RideState next(Ride ride); }
class RequestedState implements RideState { public RideState next(Ride ride) { return new MatchedState(); } }
class MatchedState implements RideState { public RideState next(Ride ride) { return new InProgressState(); } }
class InProgressState implements RideState { public RideState next(Ride ride) { return new CompletedState(); } }
class CompletedState implements RideState { public RideState next(Ride ride) { return this; } } // terminal
Directly reuses the State pattern shape from Vending Machine Design and Elevator System — a ride's behavior (which actions are even valid) genuinely differs per state, the strongest justification for State pattern per its own "when it earns its complexity" guidance.
interface RideObserver { void onStateChange(Ride ride, RideState newState); }
class RiderNotifier implements RideObserver { public void onStateChange(Ride ride, RideState state) { /* push to rider's app */ } }
class DriverNotifier implements RideObserver { public void onStateChange(Ride ride, RideState state) { /* push to driver's app */ } }
class Ride {
void transitionTo(RideState newState) {
this.state = newState;
observers.forEach(o -> o.onStateChange(this, newState)); // fires on every state transition
}
}
Every state transition needs to notify potentially multiple interested parties (rider's app, driver's app, an analytics pipeline) — the exact one-to-many, decoupled-notification shape Observer Pattern exists for.
The risk this exercise specifically probes: using three patterns because they're available, not because each solves a real, distinct problem in this specific design. The honest justification for each, stated explicitly (exactly what a strong interview answer does unprompted):
Ride's core structure.Ride needing to know about each one specifically.If any one of these three didn't have a genuine, distinct justification like this, the honest answer would be to drop it — pattern-stacking without justification is over-engineering, and naming that risk explicitly (as this lesson does) is itself part of demonstrating mature design judgment.
Q: Could Command pattern also reasonably fit somewhere in this design? A: Possibly, for something like ride cancellation-with-reason-tracking or an audit log of ride actions — but the exercise's own discipline applies: it would need its own genuine justification (a real need to queue, log, or undo specific actions) rather than being added just because Command is available, echoing the 'discuss over-engineering' framing throughout this course.
Q: Why is transitionTo() the single method that both changes state AND notifies observers, rather than two separate calls? A: Coupling them in one method guarantees observers are ALWAYS notified on a state change — if callers had to remember to call notifyObservers() separately after changing state, a forgotten call would silently desync what the UI shows from the ride's actual state, a real correctness risk this design avoids by construction.
Q: How would you unit test RequestedState.next() in isolation from the full Ride class? A: Since each RideState implementation only depends on the Ride passed into next() (and could be tested with a minimal stub Ride), each state's transition logic is independently testable — one of the practical benefits of State pattern's structure beyond just organizational clarity.
Q: Does adding Observer here risk the same memory-leak pitfall covered earlier in this course? A: Yes, directly — a RiderNotifier or DriverNotifier that's added to observers but never removed after a ride completes (or a rider closes their app) would leak exactly like Observer Pattern's own warning describes; a completed/terminal ride's observer list should be explicitly cleared or the observers should hold weak references, the same fix pattern already covered.