Loading learning content...
Two interrelated systems govern every ride: pricing determines what the rider pays and driver earns, while state management ensures the trip progresses correctly from request to completion. Both require careful design—pricing for flexibility and fairness, state management for consistency and robustness.
Why these topics together:
Pricing depends on trip state (estimated fare at request, actual fare at completion). State transitions trigger pricing calculations, payment processing, and notifications. Understanding both together reveals how they orchestrate the complete ride experience.
By the end of this page, you will understand multi-component fare calculation, surge pricing implementation, the Strategy pattern for pricing policies, and comprehensive state machine design that handles all trip lifecycle events including edge cases and cancellations.
Ride-sharing fares typically consist of multiple components combined into a final price. Understanding these components is essential for designing a flexible pricing system.
Core fare formula:
Fare = (BaseFare + (Distance × PerKmRate) + (Time × PerMinRate)) × SurgeMultiplier
+ BookingFee + Tolls - Discounts
This formula provides levers for various business objectives: base fare ensures minimum payout, distance and time rates reflect actual trip cost, surge balances supply and demand, and discounts enable marketing.
| Component | Description | Typical Value | When Applied |
|---|---|---|---|
| Base Fare | Minimum charge for any trip | $2-5 | Always |
| Per-Kilometer Rate | Distance-based charge | $1-2/km | Based on route distance |
| Per-Minute Rate | Time-based charge | $0.20-0.40/min | Trip duration |
| Booking Fee | Platform service fee | $1-3 | Per booking |
| Surge Multiplier | Supply/demand factor | 1.0x - 3.0x+ | High demand periods |
| Tolls/Fees | Pass-through costs | Variable | If route includes tolls |
| Minimum Fare | Floor for short trips | $5-8 | If calculated < minimum |
| Cancellation Fee | Penalty for late cancel | $3-10 | Post-threshold cancellation |
At request time, we calculate estimated fare based on predicted route distance and time. At completion, we calculate actual fare based on the real route taken. The difference is why riders sometimes see 'Your trip cost $X.XX' that differs from the initial estimate.
Different ride types, regions, and times may have different pricing rules. The Strategy pattern allows us to swap pricing algorithms without modifying the booking flow.
Why Strategy for pricing:
Each is a PricingStrategy implementation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
interface Fare { baseFare: number; distanceFare: number; timeFare: number; bookingFee: number; surgeMultiplier: number; surgeFare: number; // Additional surge amount tolls: number; discount: number; totalFare: number; currency: string;} interface TripDetails { distanceKm: number; durationMinutes: number; vehicleType: VehicleType; pickupLocation: Location; isEstimate: boolean;} interface PricingStrategy { calculateFare(details: TripDetails, surgeMultiplier: number): Fare; getName(): string;} /** * Standard ride pricing */class StandardPricingStrategy implements PricingStrategy { private readonly baseFare = 2.50; private readonly perKmRate = 1.20; private readonly perMinRate = 0.25; private readonly bookingFee = 1.50; private readonly minimumFare = 6.00; getName(): string { return 'STANDARD'; } calculateFare(details: TripDetails, surgeMultiplier: number): Fare { const distanceFare = details.distanceKm * this.perKmRate; const timeFare = details.durationMinutes * this.perMinRate; const subtotal = this.baseFare + distanceFare + timeFare; const surgedSubtotal = subtotal * surgeMultiplier; const surgeFare = surgedSubtotal - subtotal; let totalFare = surgedSubtotal + this.bookingFee; totalFare = Math.max(totalFare, this.minimumFare); return { baseFare: this.baseFare, distanceFare, timeFare, bookingFee: this.bookingFee, surgeMultiplier, surgeFare, tolls: 0, discount: 0, totalFare: Math.round(totalFare * 100) / 100, currency: 'USD', }; }} /** * Premium/luxury vehicle pricing */class PremiumPricingStrategy implements PricingStrategy { private readonly baseFare = 5.00; private readonly perKmRate = 2.50; private readonly perMinRate = 0.45; private readonly bookingFee = 2.50; private readonly minimumFare = 12.00; getName(): string { return 'PREMIUM'; } calculateFare(details: TripDetails, surgeMultiplier: number): Fare { const distanceFare = details.distanceKm * this.perKmRate; const timeFare = details.durationMinutes * this.perMinRate; const subtotal = this.baseFare + distanceFare + timeFare; const surgedSubtotal = subtotal * surgeMultiplier; const surgeFare = surgedSubtotal - subtotal; let totalFare = surgedSubtotal + this.bookingFee; totalFare = Math.max(totalFare, this.minimumFare); return { baseFare: this.baseFare, distanceFare, timeFare, bookingFee: this.bookingFee, surgeMultiplier, surgeFare, tolls: 0, discount: 0, totalFare: Math.round(totalFare * 100) / 100, currency: 'USD', }; }} /** * Factory to get appropriate pricing strategy */class PricingStrategyFactory { private strategies: Map<VehicleType, PricingStrategy> = new Map(); constructor() { this.strategies.set(VehicleType.STANDARD, new StandardPricingStrategy()); this.strategies.set(VehicleType.PREMIUM, new PremiumPricingStrategy()); // Add more as needed } getStrategy(vehicleType: VehicleType): PricingStrategy { return this.strategies.get(vehicleType) || new StandardPricingStrategy(); }}Surge pricing (or dynamic pricing) adjusts fares based on supply and demand. When rider demand exceeds driver supply in an area, prices increase to incentivize more drivers to that area and discourage price-sensitive riders, helping balance the market.
Surge calculation factors:
Design considerations:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
interface Zone { id: string; name: string; polygon: Location[]; // Bounding polygon} class SurgePricingService { private zoneSurgeMultipliers: Map<string, number> = new Map(); private readonly updateIntervalMs = 60000; // Update every minute private readonly maxSurge = 3.0; private readonly surgeThresholds: { ratio: number; multiplier: number }[] = [ { ratio: 0.0, multiplier: 1.0 }, // Supply >= Demand { ratio: 1.5, multiplier: 1.25 }, // 1.5x demand vs supply { ratio: 2.0, multiplier: 1.5 }, { ratio: 2.5, multiplier: 1.75 }, { ratio: 3.0, multiplier: 2.0 }, { ratio: 4.0, multiplier: 2.5 }, { ratio: 5.0, multiplier: 3.0 }, // 5x+ demand = max surge ]; /** * Get current surge multiplier for a location. */ getSurgeMultiplier(location: Location): number { const zone = this.findZone(location); if (!zone) return 1.0; return this.zoneSurgeMultipliers.get(zone.id) || 1.0; } /** * Recalculate surge for all zones. * Called periodically by a background job. */ async recalculateSurge( demandByZone: Map<string, number>, supplyByZone: Map<string, number> ): Promise<void> { for (const [zoneId, demand] of demandByZone) { const supply = supplyByZone.get(zoneId) || 0; let ratio: number; if (supply === 0) { ratio = demand > 0 ? Infinity : 0; } else { ratio = demand / supply; } const multiplier = this.ratioToMultiplier(ratio); this.zoneSurgeMultipliers.set(zoneId, multiplier); } } private ratioToMultiplier(ratio: number): number { for (let i = this.surgeThresholds.length - 1; i >= 0; i--) { if (ratio >= this.surgeThresholds[i].ratio) { return Math.min(this.surgeThresholds[i].multiplier, this.maxSurge); } } return 1.0; } private findZone(location: Location): Zone | null { // Implementation would check which zone polygon contains the location return null; // Simplified } /** * Check if surge requires rider confirmation. */ requiresConfirmation(multiplier: number): boolean { return multiplier > 1.0; }}Surge pricing is controversial. Some view it as efficient market pricing; others see it as exploitative during emergencies. Many ride-sharing companies now cap surge during declared emergencies and disasters. Your design should support configurable caps and surge suspension.
The trip state machine is the control center of ride lifecycle management. Every action—driver acceptance, arrival, trip start, completion, cancellation—is a state transition with validation, side effects, and notifications.
State machine implementation approaches:
For interviews, Enum + careful validation is usually sufficient. Let's see a robust implementation:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
enum TripState { REQUESTED = 'REQUESTED', MATCHING = 'MATCHING', DRIVER_ASSIGNED = 'DRIVER_ASSIGNED', DRIVER_ARRIVED = 'DRIVER_ARRIVED', TRIP_IN_PROGRESS = 'TRIP_IN_PROGRESS', TRIP_COMPLETED = 'TRIP_COMPLETED', CANCELLED_BY_RIDER = 'CANCELLED_BY_RIDER', CANCELLED_BY_DRIVER = 'CANCELLED_BY_DRIVER', CANCELLED_NO_SHOW = 'CANCELLED_NO_SHOW', NO_DRIVERS_AVAILABLE = 'NO_DRIVERS_AVAILABLE',} enum TripEvent { START_MATCHING = 'START_MATCHING', DRIVER_ASSIGNED = 'DRIVER_ASSIGNED', MATCHING_FAILED = 'MATCHING_FAILED', DRIVER_ARRIVED = 'DRIVER_ARRIVED', START_TRIP = 'START_TRIP', COMPLETE_TRIP = 'COMPLETE_TRIP', RIDER_CANCEL = 'RIDER_CANCEL', DRIVER_CANCEL = 'DRIVER_CANCEL', NO_SHOW = 'NO_SHOW',} interface StateTransition { from: TripState; event: TripEvent; to: TripState; guard?: (trip: Trip) => boolean; action?: (trip: Trip) => Promise<void>;} class TripStateMachine { private transitions: StateTransition[] = [ // Happy path { from: TripState.REQUESTED, event: TripEvent.START_MATCHING, to: TripState.MATCHING }, { from: TripState.MATCHING, event: TripEvent.DRIVER_ASSIGNED, to: TripState.DRIVER_ASSIGNED }, { from: TripState.DRIVER_ASSIGNED, event: TripEvent.DRIVER_ARRIVED, to: TripState.DRIVER_ARRIVED }, { from: TripState.DRIVER_ARRIVED, event: TripEvent.START_TRIP, to: TripState.TRIP_IN_PROGRESS }, { from: TripState.TRIP_IN_PROGRESS, event: TripEvent.COMPLETE_TRIP, to: TripState.TRIP_COMPLETED }, // Matching failures { from: TripState.MATCHING, event: TripEvent.MATCHING_FAILED, to: TripState.NO_DRIVERS_AVAILABLE }, // Rider cancellation (from multiple states) { from: TripState.REQUESTED, event: TripEvent.RIDER_CANCEL, to: TripState.CANCELLED_BY_RIDER }, { from: TripState.MATCHING, event: TripEvent.RIDER_CANCEL, to: TripState.CANCELLED_BY_RIDER }, { from: TripState.DRIVER_ASSIGNED, event: TripEvent.RIDER_CANCEL, to: TripState.CANCELLED_BY_RIDER }, { from: TripState.DRIVER_ARRIVED, event: TripEvent.RIDER_CANCEL, to: TripState.CANCELLED_BY_RIDER }, // Driver cancellation { from: TripState.DRIVER_ASSIGNED, event: TripEvent.DRIVER_CANCEL, to: TripState.CANCELLED_BY_DRIVER }, { from: TripState.DRIVER_ARRIVED, event: TripEvent.NO_SHOW, to: TripState.CANCELLED_NO_SHOW }, ]; async transition(trip: Trip, event: TripEvent): Promise<TripState> { const currentState = trip.getState(); const transition = this.transitions.find( t => t.from === currentState && t.event === event ); if (!transition) { throw new Error( `Invalid transition: ${event} not allowed from ${currentState}` ); } // Check guard condition if present if (transition.guard && !transition.guard(trip)) { throw new Error(`Guard condition failed for ${event}`); } // Execute action if present if (transition.action) { await transition.action(trip); } return transition.to; } getValidEvents(state: TripState): TripEvent[] { return this.transitions .filter(t => t.from === state) .map(t => t.event); } isTerminal(state: TripState): boolean { return !this.transitions.some(t => t.from === state); }}State transitions trigger important side effects: updating entity statuses, sending notifications, processing payments, and recording events. A well-designed system handles these consistently.
| Transition | Side Effects |
|---|---|
| MATCHING → DRIVER_ASSIGNED | Update driver status to ASSIGNED; Notify rider with driver details; Start ETA tracking |
| DRIVER_ASSIGNED → DRIVER_ARRIVED | Notify rider driver has arrived; Start wait timer for no-show |
| DRIVER_ARRIVED → TRIP_IN_PROGRESS | Begin route tracking; Update rider/driver UI to trip mode |
| TRIP_IN_PROGRESS → TRIP_COMPLETED | Calculate final fare; Process payment; Credit driver; Prompt ratings; Archive trip |
| Any → CANCELLED_BY_RIDER | Free driver; Possibly charge cancellation fee; Notify driver; Log reason |
| Any → CANCELLED_BY_DRIVER | Notify rider; Attempt rematch; Record against driver metrics |
| DRIVER_ARRIVED → CANCELLED_NO_SHOW | Charge rider cancellation fee; Free driver; Record as rider no-show |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
class TripService { private stateMachine: TripStateMachine; private notificationService: NotificationService; private paymentService: PaymentService; private fareCalculator: FareCalculator; async completeTrip(trip: Trip): Promise<void> { // 1. Validate transition const newState = await this.stateMachine.transition( trip, TripEvent.COMPLETE_TRIP ); // 2. Calculate final fare const actualFare = this.fareCalculator.calculateActualFare(trip); trip.setActualFare(actualFare); trip.setState(newState); // 3. Process payment await this.paymentService.charge( trip.getRider(), actualFare.totalFare, trip.getId() ); // 4. Credit driver (minus platform fee) const driverEarning = actualFare.totalFare * 0.75; // 75% to driver await this.paymentService.credit( trip.getDriver()!, driverEarning, trip.getId() ); // 5. Update statuses trip.getDriver()!.setStatus(DriverStatus.AVAILABLE); trip.getRider().setStatus(RiderStatus.IDLE); // 6. Request ratings await this.notificationService.sendRatingRequest(trip.getRider(), trip); await this.notificationService.sendRatingRequest(trip.getDriver()!, trip); // 7. Record for analytics await this.recordTripCompletion(trip); } async cancelByRider(trip: Trip, reason?: string): Promise<void> { const newState = await this.stateMachine.transition( trip, TripEvent.RIDER_CANCEL ); trip.setState(newState); // Check if cancellation fee applies const shouldChargeFee = this.shouldChargeCancellationFee(trip); if (shouldChargeFee) { await this.paymentService.charge( trip.getRider(), this.getCancellationFee(trip), trip.getId() ); } // Free the driver if one was assigned if (trip.getDriver()) { trip.getDriver()!.setStatus(DriverStatus.AVAILABLE); await this.notificationService.notifyRiderCancelled(trip.getDriver()!); } trip.getRider().setStatus(RiderStatus.IDLE); } private shouldChargeCancellationFee(trip: Trip): boolean { // Free cancellation within 2 minutes of driver assignment if (!trip.getMatchedAt()) return false; const elapsedMs = Date.now() - trip.getMatchedAt()!.getTime(); return elapsedMs > 120000; // 2 minutes } private getCancellationFee(trip: Trip): number { return 5.00; // Configurable }}What's next:
We'll consolidate the design patterns used throughout—Strategy, Observer, State, and Factory—showing how they work together to create a maintainable, extensible ride-sharing system.
You now understand ride-sharing pricing (multi-component fares, Strategy pattern, surge pricing) and state management (explicit state machine, transition validation, side effects). These systems work together to manage the complete lifecycle of every ride.