Loading content...
Throughout our ride-sharing design, we've applied design patterns at critical points. Now we consolidate this understanding, showing how Strategy, Observer, State, and Factory patterns work together to create a system that is flexible, maintainable, and extensible.
Why patterns matter for complex systems:
Patterns aren't decorative—they solve specific structural problems. In ride-sharing, variability in matching algorithms, pricing models, state handling, and notifications would create unmanageable complexity without patterns. Each pattern isolates a dimension of variability, allowing that aspect to change independently.
By the end of this page, you will see how Strategy pattern enables algorithm swapping (matching, pricing), Observer pattern decouples event notification, State pattern manages trip lifecycle, and Factory pattern centralizes object creation. You'll understand when and why each pattern is applied.
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.
Strategy applications in ride-sharing:
1234567891011121314151617181920212223242526272829303132333435363738394041
// Strategy interface for matchinginterface MatchingStrategy { rankDrivers(candidates: DriverWithDistance[], request: RideRequest): DriverWithDistance[];} // Concrete strategiesclass NearestDriverStrategy implements MatchingStrategy { /* ... */ }class WeightedScoreStrategy implements MatchingStrategy { /* ... */ }class FairDistributionStrategy implements MatchingStrategy { /* ... */ } // Context that uses the strategyclass MatchingEngine { private strategy: MatchingStrategy; constructor(strategy: MatchingStrategy) { this.strategy = strategy; } // Strategy can be changed at runtime setStrategy(strategy: MatchingStrategy): void { this.strategy = strategy; } async findMatch(request: RideRequest): Promise<MatchResult> { const candidates = await this.findCandidates(request); // Delegate ranking to strategy const ranked = this.strategy.rankDrivers(candidates, request); return this.processRankedCandidates(ranked); }} // Usage: swap strategies based on conditionsfunction selectMatchingStrategy(context: MatchingContext): MatchingStrategy { if (context.isPeakHours) { return new NearestDriverStrategy(); // Speed matters most } else if (context.isLowSupply) { return new FairDistributionStrategy(); // Keep all drivers engaged } else { return new WeightedScoreStrategy(); // Balance all factors }}Notice how策略 selection can be dynamic—based on time of day, supply conditions, or A/B test cohorts. This enables experimentation without code changes to the matching engine itself.
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
Observer applications in ride-sharing:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
// Event types for the trip lifecycleinterface TripEvent { tripId: string; eventType: TripEventType; timestamp: Date; data: Record<string, unknown>;} enum TripEventType { DRIVER_ASSIGNED = 'DRIVER_ASSIGNED', DRIVER_ARRIVED = 'DRIVER_ARRIVED', TRIP_STARTED = 'TRIP_STARTED', TRIP_COMPLETED = 'TRIP_COMPLETED', TRIP_CANCELLED = 'TRIP_CANCELLED', LOCATION_UPDATED = 'LOCATION_UPDATED',} // Observer interfaceinterface TripEventObserver { onTripEvent(event: TripEvent): void;} // Subject (Observable)class TripEventPublisher { private observers: TripEventObserver[] = []; subscribe(observer: TripEventObserver): void { this.observers.push(observer); } unsubscribe(observer: TripEventObserver): void { this.observers = this.observers.filter(o => o !== observer); } publish(event: TripEvent): void { for (const observer of this.observers) { observer.onTripEvent(event); } }} // Concrete observersclass RiderNotificationObserver implements TripEventObserver { private notificationService: NotificationService; onTripEvent(event: TripEvent): void { switch (event.eventType) { case TripEventType.DRIVER_ASSIGNED: this.notificationService.notifyRider( event.data.riderId as string, 'Your driver is on the way!', event.data ); break; case TripEventType.DRIVER_ARRIVED: this.notificationService.notifyRider( event.data.riderId as string, 'Your driver has arrived!', event.data ); break; // ... other events } }} class DriverNotificationObserver implements TripEventObserver { onTripEvent(event: TripEvent): void { // Notify driver of relevant events }} class AnalyticsObserver implements TripEventObserver { onTripEvent(event: TripEvent): void { // Log event for analytics pipeline console.log(`Analytics: ${event.eventType} at ${event.timestamp}`); }} class AuditLogObserver implements TripEventObserver { onTripEvent(event: TripEvent): void { // Persist event for compliance and debugging }} // Usage: register observers at startupconst tripEventPublisher = new TripEventPublisher();tripEventPublisher.subscribe(new RiderNotificationObserver(notificationService));tripEventPublisher.subscribe(new DriverNotificationObserver(notificationService));tripEventPublisher.subscribe(new AnalyticsObserver());tripEventPublisher.subscribe(new AuditLogObserver());The Trip entity doesn't know about notifications, analytics, or auditing. It just publishes events. New concerns (e.g., fraud detection, real-time dashboards) can be added as new observers without modifying Trip or existing observers. This is the Open/Closed Principle in action.
The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
State pattern in ride-sharing:
While we implemented the trip state machine using enums and explicit transitions, a more sophisticated approach uses the State pattern where each state is a class with allowed behaviors.
Benefits:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// State interfaceinterface TripState { getName(): string; // Actions with default implementations throwing "not allowed" onDriverAssigned(trip: Trip, driver: Driver): TripState; onDriverArrived(trip: Trip): TripState; onTripStarted(trip: Trip): TripState; onTripCompleted(trip: Trip): TripState; onCancelledByRider(trip: Trip): TripState; onCancelledByDriver(trip: Trip): TripState;} // Abstract base with default "not allowed" implementationsabstract class BaseTripState implements TripState { abstract getName(): string; onDriverAssigned(trip: Trip, driver: Driver): TripState { throw new Error(`Cannot assign driver in ${this.getName()} state`); } onDriverArrived(trip: Trip): TripState { throw new Error(`Driver cannot arrive in ${this.getName()} state`); } onTripStarted(trip: Trip): TripState { throw new Error(`Cannot start trip in ${this.getName()} state`); } onTripCompleted(trip: Trip): TripState { throw new Error(`Cannot complete trip in ${this.getName()} state`); } onCancelledByRider(trip: Trip): TripState { throw new Error(`Cannot cancel in ${this.getName()} state`); } onCancelledByDriver(trip: Trip): TripState { throw new Error(`Cannot cancel in ${this.getName()} state`); }} // Concrete statesclass MatchingState extends BaseTripState { getName(): string { return 'MATCHING'; } onDriverAssigned(trip: Trip, driver: Driver): TripState { trip.setDriver(driver); trip.setMatchedAt(new Date()); return new DriverAssignedState(); } onCancelledByRider(trip: Trip): TripState { trip.setCancelledAt(new Date()); return new CancelledByRiderState(); }} class DriverAssignedState extends BaseTripState { getName(): string { return 'DRIVER_ASSIGNED'; } onDriverArrived(trip: Trip): TripState { trip.setDriverArrivedAt(new Date()); return new DriverArrivedState(); } onCancelledByRider(trip: Trip): TripState { // May trigger cancellation fee trip.setCancelledAt(new Date()); return new CancelledByRiderState(); } onCancelledByDriver(trip: Trip): TripState { trip.setCancelledAt(new Date()); return new CancelledByDriverState(); }} class DriverArrivedState extends BaseTripState { getName(): string { return 'DRIVER_ARRIVED'; } onTripStarted(trip: Trip): TripState { trip.setTripStartedAt(new Date()); return new TripInProgressState(); } onCancelledByRider(trip: Trip): TripState { // Definitely charges cancellation fee trip.setCancelledAt(new Date()); return new CancelledByRiderState(); }} class TripInProgressState extends BaseTripState { getName(): string { return 'TRIP_IN_PROGRESS'; } onTripCompleted(trip: Trip): TripState { trip.setTripCompletedAt(new Date()); return new TripCompletedState(); }} class TripCompletedState extends BaseTripState { getName(): string { return 'TRIP_COMPLETED'; } // Terminal state - no transitions allowed} // Trip context using state patternclass Trip { private state: TripState; // ... other fields constructor() { this.state = new RequestedState(); } assignDriver(driver: Driver): void { this.state = this.state.onDriverAssigned(this, driver); } driverArrived(): void { this.state = this.state.onDriverArrived(this); } startTrip(): void { this.state = this.state.onTripStarted(this); } completeTrip(): void { this.state = this.state.onTripCompleted(this); } getStateName(): string { return this.state.getName(); }}The Factory pattern provides an interface for creating objects without specifying their exact classes. It centralizes creation logic and can return different implementations based on parameters.
Factory applications in ride-sharing:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
class TripFactory { private pricingStrategyFactory: PricingStrategyFactory; private fareCalculator: FareCalculator; private idGenerator: IdGenerator; createTrip( rider: Rider, pickupLocation: Location, destination: Location, vehicleType: VehicleType ): Trip { // Generate unique ID const tripId = this.idGenerator.generate('trip'); // Get appropriate pricing strategy const pricingStrategy = this.pricingStrategyFactory.getStrategy(vehicleType); // Calculate estimated fare const estimatedDistance = pickupLocation.distanceTo(destination); const estimatedDuration = this.estimateDuration(estimatedDistance); const surgeMultiplier = this.getSurgeMultiplier(pickupLocation); const estimatedFare = pricingStrategy.calculateFare({ distanceKm: estimatedDistance, durationMinutes: estimatedDuration, vehicleType, pickupLocation, isEstimate: true, }, surgeMultiplier); // Create and configure trip const trip = new Trip( tripId, rider, pickupLocation, destination, estimatedFare ); // Additional initialization trip.setVehicleType(vehicleType); trip.setSurgeMultiplier(surgeMultiplier); return trip; } private estimateDuration(distanceKm: number): number { // Rough estimate: 25 km/h average city speed return (distanceKm / 25) * 60; } private getSurgeMultiplier(location: Location): number { // Would query SurgePricingService return 1.0; }} // Strategy factory - returns appropriate strategyclass MatchingStrategyFactory { createStrategy(context: MatchingContext): MatchingStrategy { if (context.rideType === 'POOL') { return new PoolMatchingStrategy(); } else if (context.isPeakHours) { return new NearestDriverStrategy(); } else { return new WeightedScoreStrategy(); } }}The power of patterns emerges when they work together. Here's how our patterns collaborate:
Request → Trip flow:
This separation means:
| Pattern | Responsibility | Examples in System |
|---|---|---|
| Strategy | Encapsulate interchangeable algorithms | MatchingStrategy, PricingStrategy, SurgeStrategy |
| Observer | Decouple event producers from consumers | TripEventPublisher → Notifications, Analytics, Audit |
| State | Manage object lifecycle with state-specific behavior | TripState classes managing transitions |
| Factory | Centralize and encapsulate object creation | TripFactory, StrategyFactory, FareFactory |
When explaining your design, explicitly name patterns and their purpose: 'I'm using Strategy for pricing because different vehicle types have different rate structures, and I want to add new tiers without modifying existing code.' This demonstrates pattern fluency and SOLID understanding.
What's next:
In our final page, we'll consolidate everything into a complete design walkthrough—showing the full system architecture with class diagrams, sequence diagrams, and interview presentation guidance.
You now understand how Strategy, Observer, State, and Factory patterns work together in the ride-sharing system. Each pattern addresses a specific design challenge, and together they create a flexible, maintainable architecture. Next, we present the complete integrated design.