Scope it first
"The object design of ride-hailing: trips and their lifecycle, riders/drivers, matching policy, fare calculation with surge, ratings. The geo-index and scale story is the HLD doc — here we design the domain model a single region's service runs. OK?"
The grading centers: a rich state machine (the trip has more states, actors and illegal transitions than any other classic), and fare calculation — the cleanest real-world showcase of Strategy + Decorator composition in the catalog.
Entities & relationships
Modeling calls to narrate:
Tripis the aggregate root — the entity every other object hangs off, the unit of consistency (the precious data), and the only place state transitions happen. Riders don't set drivers; trips assign drivers.Driver.statusis a second, smaller state machine (AVAILABLE → OFFERED → ON_TRIP) that must stay consistent with trips — the OFFERED reservation is the double-booking guard.- Two strategies, two interfaces:
MatchingStrategy(nearest / highest-rated / batched assignment) andFarePolicy(below) vary independently — don't fuse them into one "ConfigService."
The trip state machine (the rich one)
Drive a trimmed version of that machine below — request → match → pickup →
dropoff. Then try a cancel from different states (it's legal from several, but
means something different in each), or an illegal dropoff before pickup, which
the machine simply refuses:
1/4Start in Req. Each event is handled by the current state — the State pattern moves this branching out of one giant switch and into the state objects themselves.
What makes this machine interesting — and the follow-up territory:
- Transitions have actors and guards:
start()is driver-only and legal only from ARRIVING;cancel()is legal from many states but means something different in each — that's not one method, it's aCancellationPolicyconsulted per state (free at REQUESTED, fee after the driver's driven 5 minutes toward you). Encoding "cancel" as state-dependent policy rather than an if-ladder is the design move graders wait for. - Every transition emits an event (
TripAssigned,TripCompleted) — notifications, receipts, analytics and driver payouts subscribe (Observer → event-driven seams);COMPLETEDis what triggers payment, and a payment failure does not un-complete the trip — money flows are compensated, never rewound (the saga stance). - Trip + driver transitions must be atomic at assignment
(
MATCHING→ASSIGNEDwithOFFERED→ON_TRIP) — same transaction or conditional update; this is where the HLD's matching race touches down in the code.
Fare calculation: strategies that compose
A fare isn't one formula — it's an ordered pipeline of policies, each transforming a running total:
class FarePolicy(ABC):
@abstractmethod
def apply(self, trip: Trip, fare: Money) -> Money: ...
class BaseFare(FarePolicy): # flat amount by city/vehicle class
def apply(self, trip, fare): return fare + self.base[trip.vehicle_class]
class DistanceTime(FarePolicy): # per-km + per-minute
def apply(self, trip, fare):
return fare + trip.km * self.per_km + trip.minutes * self.per_min
class SurgeMultiplier(FarePolicy): # multiplies everything BEFORE it
def apply(self, trip, fare): return fare * trip.surge_at_request
class PromoDiscount(FarePolicy): # subtracts AFTER surge, floor at minimum
def apply(self, trip, fare):
return max(fare - self.promo.value(fare), self.minimum_fare)
calculator = FareCalculator(policies=[
BaseFare(...), DistanceTime(...), SurgeMultiplier(), PromoDiscount(...)
]) # ORDER IS PART OF THE CONTRACT
Two sentences make this section senior-grade: order is semantics (surge-then-promo vs promo-then-surge are different prices — the list order is a business rule under test), and the surge multiplier is captured at request time onto the trip — the rider pays the price they were quoted, not the price at completion (the quoted-price-is-a-promise rule; money facts freeze when shown). All arithmetic in integer paise with explicit rounding policy — the Splitwise laws apply unchanged.
Think it through like the interview
PROBLEMDesign the domain model for ride-hailing: trips and their lifecycle, riders and drivers, matching policy, fares with surge, ratings. Geo-indexing at scale is out of scope.
- 1
Pick the aggregate root
“Trips, riders, drivers, fares, ratings — which object is the center of gravity?”
- 2
Draw the state machine with actors
“REQUESTED → … → COMPLETED. But who is allowed to trigger each transition?”
unlocks after the stage above - 3
One verb, many meanings → policy object
“cancel() is legal from four states and means something different in each. Method or something more?”
unlocks after the stage above - 4
Fares = ordered pipeline of policies
“Base + distance + time, ×surge, −promo, floor at minimum. What structure is that?”
unlocks after the stage above - 5
The atomicity follow-up
“Assignment flips Trip(MATCHING→ASSIGNED) and Driver(OFFERED→ON_TRIP). What if those are two writes?”
unlocks after the stage above
Walk a scenario
Rider requests: Trip(REQUESTED), surge 1.4× stamped on it →
MATCHING; NearestDriver strategy picks from the
geo-index's candidates; offer → accept →
atomic ASSIGNED + driver ON_TRIP; TripAssigned event →
rider's app shows the car (live tracking is HLD).
Driver arrives, taps start (guard: state == ARRIVING ✓) →
IN_PROGRESS; arrival → complete() → fare pipeline runs:
base 50 + (12 km, 31 min → 230) = 280, ×1.4 surge = 392, promo −50 =
₹342 → TripCompleted → payment service charges (idempotency key =
trip id), receipt notification, both parties prompted to rate
(RatingService accepts only for COMPLETED trips, once per side —
two more guards). A cancellation at ARRIVING instead would have
consulted CancellationPolicy(ARRIVING) → ₹40 fee, driver released
to AVAILABLE, that policy decision logged onto the trip for the
inevitable support ticket.
Practice — level up
Ride-hailing is nearest-match plus a trip lifecycle: find the closest free driver, assign exactly one, then run the trip from request to fare. These drills rehearse the matching and the hand-off.
Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.
Warm-up — who's nearest
Rank candidates by distance — the nearest free drivers to a rider.
Core — assign one, track the trip
Match a rider to a driver; meter the ride.- Campus BikesMedium
Assign each worker the closest free bike — nearest-driver matching with no double-booking.
Start → end a trip and compute the fare — the ride's check-in/check-out lifecycle.
Stretch — dispatch as drivers free up
Hand the next request to a driver the moment one frees — surge dispatch over time.