Ride Hailing
This looks like food-delivery.mdx at first glance - matching, a lifecycle, a pricing rule -
but the pricing rule here is the interesting twist: the price isn't fixed at request time,
it depends on demand right now, which means quoting a ride and requesting one can't be the
same step.
Requirements
Functional
- A
Riderrequests aRidefrom a pickup to a drop-off location; the system matches it to the nearest availableDriver. - The fare is computed from distance and a surge multiplier that reflects current demand in that area.
- A ride moves through a lifecycle: requested, driver assigned, in progress, completed (or cancelled before a driver arrives).
- A cancelled ride releases its driver back to available without charging a fare.
Non-functional
- Driver matching must be swappable (nearest-available today, a rating-weighted match tomorrow) without changing how a ride is requested.
- Surge pricing must be swappable independently of matching - a busy area might have plenty of drivers or none, and the two concerns shouldn't be coupled into one class.
Design
Matching and pricing are two separate strategies, not one combined "figure out the ride"
method - DriverMatchingStrategy only ever answers "which driver," and PricingStrategy
only ever answers "how much," and Ride calls each exactly once at the point in its
lifecycle where that answer is needed. That separation is the whole point: a city can run a
new surge algorithm without touching who gets matched, and vice versa.
- 1A ride starts in REQUESTED with no driver and no fare yet.
- 2Matching is asked for a driver, nothing about price.
- 3Pricing is asked separately, nothing about which driver.
- 4The chosen driver updates its own availability, same as DeliveryPartner in food-delivery.mdx.
- 5The locked-in fare from assignment time is what the rider is charged, unaffected by any surge change since.
Like food-delivery.mdx, Ride.status only advances through one guarded method - but
unlike that page, cancellation here is a first-class transition available from more than one
state, since a rider can back out any time before the driver actually arrives:
Class diagram
Code
Design decisions
- Matching and pricing are two separate strategy interfaces, not one. They change for
completely different reasons and on different schedules - ops tunes matching for wait
times, finance tunes pricing for revenue - so bundling them into one
MatchAndPriceStrategywould force every pricing experiment to also touch matching code, and vice versa, for no shared reason. - The fare is computed once, at driver assignment, and stored on the
Ride- not recomputed at completion. Surge multipliers move by the minute; if fare were calculated from the surge level at drop-off instead of pickup, a rider could watch their price change mid-ride for reasons that have nothing to do with their trip. Locking it in at assignment is what makes the price the rider agreed to the price they pay. - Cancellation is reachable from
REQUESTEDandDRIVER_ASSIGNED, not just one state. A rider backing out before any driver has committed is a different (and cheaper) event than backing out after a driver is already en route, but both need a path back to a clean state - the transition table has to name both instead of assuming cancellation only ever happens from the start. - What's missing for a real system: surge here is a static multiplier read at assignment time; a real system computes it from live supply/demand in a geofenced area and needs that computation to be fast enough to run on every ride request, and cancellation after a driver is already close to pickup typically carries its own small fee - a policy decision layered on top of this lifecycle, not a change to the lifecycle itself.
Common follow-ups
- Why is the fare locked in at driver assignment instead of computed fresh when the ride
completes? Surge multipliers move by the minute; if fare were calculated from the surge
level at drop-off instead of pickup, a rider could watch their price change mid-ride for
reasons that have nothing to do with their own trip. Locking it in at
requestDrivertime is what makes the price the rider agreed to the price they actually pay. - How would you charge a cancellation fee only when the rider cancels after a driver is
already close to pickup? This is a policy decision layered on the existing transition,
not a new state -
transitionTo(CANCELLED)already fires from bothREQUESTEDandDRIVER_ASSIGNED; the fee logic would live in the caller that invokes cancellation, checking how long the ride has been inDRIVER_ASSIGNED(or the driver's current distance from pickup) before deciding whether to charge, rather than becoming a third cancellation state onRideitself. - Two riders request a ride from the same pickup area within seconds, and both get
matched to the same nearest driver - what stops that? As written, nothing does:
matchandmarkBusyare separate steps, so bothrequestDrivercalls could see the same driver as available before either flips it. Same fix as the seat-locking race inmovie-booking.mdxand the partner-matching race infood-delivery.mdx- matching and marking busy need to be one atomic operation onDriver. - How would you support ride-pooling (two riders sharing one driver, different
drop-offs)? This changes
Ridefrom "one rider, one driver" to something that can hold multiple rider/drop-off pairs against a singleDriver, andPricingStrategy.quotewould need each rider's individual distance rather than one shareddistanceKm- a bigger structural change than swapping a strategy implementation, since it touches whatRideitself represents, not just how one of its two strategies computes an answer.
Check yourself
Why are driver matching and fare pricing two separate strategy interfaces instead of one combined `MatchAndPriceStrategy`?