Design Uber (LLD)

The trip as a state machine, fare calculation as composable strategies, matching behind an interface — the class design beneath the geospatial giant.

LLDOODstate-machinestrategy

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:

  • Trip is 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.status is 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) and FarePolicy (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:

Uber trip lifecycletime O(1) per eventspace O(states)
matchpickupdropoffcancelcancelReqMatchedOnTripEnded
events:matchpickupdropoff

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.

state = Req

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 a CancellationPolicy consulted 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 (Observerevent-driven seams); COMPLETED is 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→ASSIGNED with OFFERED→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:

Python
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

Think it through: Design Uber (LLD)LLD Classic — rich state machine0/5 stages

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. 1

    Pick the aggregate root

    Trips, riders, drivers, fares, ratings — which object is the center of gravity?

  2. 2

    Draw the state machine with actors

    REQUESTED → … → COMPLETED. But who is allowed to trigger each transition?

    unlocks after the stage above
  3. 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. 4

    Fares = ordered pipeline of policies

    Base + distance + time, ×surge, −promo, floor at minimum. What structure is that?

    unlocks after the stage above
  5. 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.

Practice ladder: Matching & trip lifecycle0/4 solved

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

  1. 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.
  1. Assign each worker the closest free bike — nearest-driver matching with no double-booking.

  2. Start → end a trip and compute the fare — the ride's check-in/check-out lifecycle.

Stretch — dispatch as drivers free up

  1. Hand the next request to a driver the moment one frees — surge dispatch over time.