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.