5. Ride-Sharing Service (Uber/Lyft) Advanced

A. Realistic Interview Prompt

"Design a ride-sharing service like Uber or Lyft. Riders request a trip from a pickup location to a destination; the system matches them with a nearby available driver, the ride proceeds through pickup and drop-off, and a fare is calculated and charged at the end."

B. Difficulty

Advanced. This is one of the few problems in this set that genuinely straddles LLD and HLD: it combines a real-time matching problem, a multi-state ride lifecycle, geospatial data structures, dynamic pricing, and heavy concurrency (many riders and drivers mutating shared state at once). Interviewers explicitly watch for whether a candidate recognizes the LLD/HLD boundary rather than trying to design a distributed geo-sharded system inside a 45-minute class-design round.

C. What This Problem Tests

D. Requirements

Functional

  1. Rider requests a ride: pickup location, dropoff location
  2. System matches the rider to a nearby available driver
  3. Driver accepts or rejects the match; on accept, drives to pickup
  4. Ride proceeds: driver arrives → picks up rider → trip in progress → drop-off
  5. Fare is computed (distance + time + surge) and charged
  6. Either party may cancel before the ride starts
  7. Rider and driver rate each other after completion

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Do we need a real geospatial index (geohash/quadtree), or can we assume "a list of nearby drivers" is already available?Determines how much of GeoIndex to actually build vs. wave hands atGeoIndex scope
2Is surge pricing required, or is a flat distance+time fare acceptable?Changes whether FareStrategy needs a composable modifierDecorator vs. plain Strategy for fare
3Can a driver reject a match? What happens then?Determines whether matching is a single lookup or a retry loopRideService.requestRide control flow
4Is continuous real-time GPS tracking in scope, or just pickup/dropoff points?A full location-streaming pipeline is an HLD concern, not a class-design oneLocationIndex update frequency assumption
5Do we need multiple vehicle/ride types (e.g. Economy, XL, Pool)?Affects how matching and fare are parameterizedVehicle.type, matching filter
6Is payment processing itself in scope, or just fare calculation?Keeps the class boundary honest — this problem is about the ride lifecycle, not a payments systemPaymentGateway as an external interface
7What happens if no driver is available nearby?Determines whether there's a timeout/expansion-radius policyRideService retry policy
8Are there cancellation fees for late cancellations?Adds a branch to the fare/cancellation logicRide state machine + FareStrategy
9Is driver earnings/ride-history tracking required?Affects persistence scopeOut of core scope unless asked
10Are ratings part of this round?A small but real actor responsibility if includedRating as a lightweight value object

F. Assumptions

G. Actors

ActorResponsibility
RiderRequests rides, cancels, pays fare, rates driver
DriverGoes online/offline, accepts or rejects matches, updates location, completes rides, rates rider
System / RideServiceMatches, enforces state transitions, computes fare, triggers notifications

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
UserShared identityid, name, phone, rating—Role-specific behavior
RiderRider-specific stateuserId, paymentMethodrequestRide()Matching, fare math
DriverDriver-specific stateuserId, vehicle, currentLocation, statusaccept(), reject(), updateLocation()Matching decisions (system decides, driver only accepts/rejects)
VehicleVehicle datamake, model, plate, type—Behavior
LocationImmutable geo point value objectlat, lngdistanceTo(other)Storage/indexing (that's GeoIndex's job)
RideOne trip's full record and staterider, driver, pickup, dropoff, status, fare, timestampstransitionTo(status) (guarded)Matching, fare computation (delegates to strategies)
GeoIndexSpatial lookup of available driversbucketed location mapfindNearby(loc, radius), update(driverId, loc)Ranking/selecting among candidates (that's MatchingStrategy)
MatchingStrategy (interface)Selects a driver from nearby candidates—match(ride, candidates)Reserving the driver (service does that atomically)
FareStrategy (interface)Computes base fare—calculate(ride)Surge multiplication (delegated to a decorator)
NotificationService (Observer)Reacts to ride events—onRideStatusChanged(ride)Deciding what happened — only reacts
RideService (facade)Orchestrates the whole flow—requestRide(), acceptRide(), startRide(), completeRide(), cancelRide()Matching math, fare math, notification delivery — all delegated

I. Class Relationships

J. Class Diagram

wraps

User

id

name

phone

rating

Rider

paymentMethod

Driver

vehicle

currentLocation

status

Vehicle

make

model

plate

type

Location

lat

lng

+distanceTo(other)

Ride

rider

driver

pickup

dropoff

status

fare

+transitionTo(status)

GeoIndex

+findNearby(loc, radius)

+update(driverId, loc)

«interface»

MatchingStrategy

+match(ride, candidates)

«interface»

FareStrategy

+calculate(ride)

SurgeFareDecorator

-wrapped: FareStrategy

+calculate(ride)

NotificationService

+onRideStatusChanged(ride)

RideService

+requestRide()

+acceptRide()

+startRide()

+completeRide()

+cancelRide()

NearestDriverStrategy

DistanceTimeFareStrategy

View Mermaid source
classDiagram
  class User {
    id
    name
    phone
    rating
  }
  class Rider {
    paymentMethod
  }
  class Driver {
    vehicle
    currentLocation
    status
  }
  class Vehicle {
    make
    model
    plate
    type
  }
  class Location {
    lat
    lng
    +distanceTo(other)
  }
  class Ride {
    rider
    driver
    pickup
    dropoff
    status
    fare
    +transitionTo(status)
  }
  class GeoIndex {
    +findNearby(loc, radius)
    +update(driverId, loc)
  }
  class MatchingStrategy {
    <<interface>>
    +match(ride, candidates)
  }
  class FareStrategy {
    <<interface>>
    +calculate(ride)
  }
  class SurgeFareDecorator {
    -wrapped: FareStrategy
    +calculate(ride)
  }
  class NotificationService {
    +onRideStatusChanged(ride)
  }
  class RideService {
    +requestRide()
    +acceptRide()
    +startRide()
    +completeRide()
    +cancelRide()
  }
  User <|-- Rider
  User <|-- Driver
  Driver --> Vehicle
  Driver --> Location
  Ride --> Rider
  Ride --> Driver
  MatchingStrategy <|.. NearestDriverStrategy
  FareStrategy <|.. DistanceTimeFareStrategy
  FareStrategy <|.. SurgeFareDecorator
  SurgeFareDecorator --> FareStrategy : wraps
  RideService --> MatchingStrategy
  RideService --> FareStrategy
  RideService --> GeoIndex
  RideService --> NotificationService
  RideService --> Ride

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
RideHolds trip state, enforces legal transitionsHow a fare is computed, how a driver is chosen
GeoIndex"Who's physically nearby?""Who should I pick?" (that's ranking, a Strategy concern)
MatchingStrategy"Who should I pick?" among candidates GeoIndex returnedReserving the driver (atomicity is the service's job)
FareStrategy / decoratorsThe math of what this trip costsCharging a card, persisting the payment
RideServiceOrchestration — ties matching, reservation, state transitions, fare, and notification togetherAll the actual math and lookups, which are delegated

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPGeoIndex only finds candidates; MatchingStrategy only ranks/picks; Ride only tracks stateA Ride class that also scans all drivers and computes the fare inside its own constructor
OCPNew matching heuristic = new MatchingStrategy; new pricing modifier = new decorator; zero changes to RideServiceif (matchType == "NEAREST") ... else if (matchType == "RATING_WEIGHTED") ... inline in the service
LSPEvery FareStrategy/decorator must return a fare ≥ base cost components and honor the same signature; every MatchingStrategy must return a driver or explicitly "none found"A decorator that sometimes returns null instead of a fare, forcing every caller to null-check inconsistently
ISPOne-method MatchingStrategy and FareStrategy interfacesA fat "PricingEngine" interface also requiring refund(), invoice(), tax()
DIPRideService depends on the MatchingStrategy/FareStrategy/GeoIndex interfaces, injected at constructionRideService directly instantiating new NearestDriverStrategy() inline, or calling a static geo-lookup function

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
StrategyMatching and fare both genuinely vary by algorithmMatchingStrategy, base FareStrategyN/A — clearly justified; note these are two separate strategies, not one
DecoratorSurge is a composable modifier over any base fare, not a mutually exclusive alternativeSurgeFareDecorator wrapping FareStrategySkip if pricing is genuinely flat with no modifiers ever
StateRide lifecycle has real illegal transitions to prevent (can't complete before starting)Ride.transitionTo() guarded by a status→allowed-next-statuses mapSkip for a toy version with only two statuses — a plain enum is fine then
ObserverNotifications shouldn't couple RideService to SMS/push/email delivery detailsNotificationService subscribed to ride status changesSkip if notifications are truly out of scope for the round
FacadeCallers shouldn't juggle geo-lookup + matching + reservation + fare themselvesRideServiceSkip only for the smallest single-rider/single-driver toy version

Explicitly avoided: hardcoding RideService (or a DriverRegistry/TripManager) as a Singleton. Several reference solutions default to Singleton "because there's only one system," but that bakes in global mutable state, makes the class impossible to instantiate twice in a test, and buys nothing that ordinary dependency injection of a single instance at the composition root doesn't already give you — the same critique made of ParkingLot in Problem 1.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Matching algorithmNearest-by-distance onlyETA-aware, rating-aware, or acceptance-rate-aware rankingMatchingStrategy interface
Fare modelDistance + timeSurge, promo codes, loyalty discounts stacked as modifiersFareStrategy + decorator chain
Vehicle/ride typesSingle typeEconomy/XL/Pool with shared-ride matchingVehicle.type as a matching filter, not a new class hierarchy
Notification channelGeneric "notify"SMS → push → in-app, possibly several at onceNotificationService/Observer list, not a hardcoded call
Geospatial indexGrid-bucket approximationReal geohash/quadtree with dynamic bucket sizingGeoIndex as an interface the service depends on

O. Design Evolution

What stays stable: RideService.requestRide()/acceptRide()/completeRide() signatures. Matching heuristics, fare modifiers, and the index implementation are all additive.

P. API / Interface Design

interface MatchingStrategy {
  Optional<Driver> match(Ride ride, List<Driver> candidates);
}

interface FareStrategy {
  Money calculate(Ride ride);
}

class RideService {
  Ride requestRide(Rider rider, Location pickup, Location dropoff);
  void acceptRide(String rideId, Driver driver);
  void rejectRide(String rideId, Driver driver);     // triggers re-match
  void startRide(String rideId);                     // ARRIVED -> IN_PROGRESS
  Ride completeRide(String rideId);                  // computes + charges fare
  void cancelRide(String rideId, Actor by);
}

Ownership/validation notes: acceptRide only succeeds if the driver's status atomically transitions AVAILABLE → RESERVED — see W for why this must be a single atomic operation rather than a check-then-set. completeRide is the only place a fare is finalized and charged, keeping "trip happened" and "money moved" as one guarded step.

Q. Pseudocode — Critical Logic

requestRide(rider, pickup, dropoff):
  ride = Ride.create(rider, pickup, dropoff, status=REQUESTED)
  candidates = geoIndex.findNearby(pickup, radius=DEFAULT_RADIUS)
  while candidates not empty:
    driver = matchingStrategy.match(ride, candidates)
    if driver is None: break
    if driver.tryReserve():           // atomic CAS: AVAILABLE -> RESERVED
      ride.transitionTo(MATCHED, driver)
      notificationService.onRideStatusChanged(ride)
      return ride
    candidates.remove(driver)         // someone else grabbed it first, retry
  ride.transitionTo(NO_DRIVER_FOUND)
  return ride

completeRide(rideId):
  ride = rides.get(rideId)
  ride.transitionTo(COMPLETED)        // throws if not currently IN_PROGRESS
  baseFare = fareStrategy.calculate(ride)     // may itself be a decorator chain
  ride.fare = baseFare
  paymentGateway.charge(ride.rider, baseFare)
  driver.release()                    // RESERVED/ON_TRIP -> AVAILABLE
  notificationService.onRideStatusChanged(ride)
  return ride

R. End-to-End Use Case

Rider requests a ride from A to B
  │
  ▼
RideService.requestRide(rider, A, B)
  │
  ▼
GeoIndex.findNearby(A) → [D1, D2, D3]
  │
  ▼
NearestDriverStrategy.match → D1 (closest)
  │
  ▼
D1.tryReserve() → succeeds (atomic CAS)   // Ride: REQUESTED → MATCHED
  │
  ▼
D1 accepts → Ride: MATCHED → ACCEPTED → (arrives) → ARRIVED → IN_PROGRESS
  │
  ▼
Rider dropped off → RideService.completeRide(rideId)
  │
  ▼
Ride: IN_PROGRESS → COMPLETED
DistanceTimeFareStrategy → $18.40 → SurgeFareDecorator(×1.3) → $23.92
  │
  ▼
PaymentGateway.charge(rider, $23.92); D1.release() → AVAILABLE again

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
Grid/geohash bucket map (Map<cellId, List<driverId>>)GeoIndex.findNearbyNarrows the search to a handful of cells instead of scanning every driver in the cityQuadtree adapts to uneven driver density better but is materially more complex to implement in an interview timeframe
Min-heap by distanceRanking candidates within a bucketEfficiently pull the k-nearest without fully sortingFull sort is simpler but wasteful once candidate lists get large
Atomic/CAS field on Driver.statusReservationGuarantees exactly one requester wins the race for a given driverA coarse global lock around all drivers is simpler but serializes unrelated matches city-wide

T. Complexity Analysis

OperationTimeSpace
GeoIndex.findNearbyO(drivers in nearby cells), not O(all drivers)O(drivers)
MatchingStrategy.matchO(k log k) for k candidates (heap-based ranking)O(k)
Ride.transitionToO(1) (map lookup against allowed-transitions table)—
FareStrategy.calculate (with decorators)O(d) for d stacked decoratorsO(d)

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Add surge pricing during high demand."Recognizing a modifier vs. an alternativeSurgeFareDecorator wraps the existing FareStrategyNew decorator onlyDecorator over a second Strategy — avoids duplicating base fare math
"Two riders request near the same lone available driver at the same instant."Concurrency correctness under real contentionAtomic CAS reservation on Driver.status; loser retries next candidateDriver, RideServiceSame shape as Parking Lot's atomic check-and-occupy
"Support ride pooling (multiple riders sharing a trip)."Recognizing a real complexity jumpRide needs multiple rider legs with independent pickup/dropoff and fare-splittingRide, matching logicFlag it honestly as a bigger redesign, not a one-line tweak
"Scale matching to millions of drivers city-wide."LLD → HLD boundaryGrid-bucket index becomes a distributed geospatial service (e.g. geohash-sharded, Kafka-fed location updates)None in-process — infra changeState the boundary explicitly rather than trying to "distribute" the classes
"A driver goes offline mid-trip (app crash, no signal)."Failure-mode thinkingHeartbeat/timeout marks the driver stale; ride may need manual intervention or reassignment policyDriver, RideServiceNo clean automatic fix — be upfront that this needs an operational policy, not just code
"Add a cancellation fee for late cancellations."Extending the state machine's edges cleanlyCancellation transition carries a time-since-match check feeding a small fee calculationRide, FareStrategyKeep the fee rule out of Ride itself — it's a policy, not trip state
"What if the matched driver never responds to the accept request?"Timeout handlingAccept has a timeout; on expiry, release the reservation and retry the next candidateRideServiceWithout this, one unresponsive driver silently strands a rider forever
"Support multiple ride types (Economy/XL/Pool) with different pricing."Parameterization vs. new class explosionVehicle.type feeds both the matching filter and the fare strategy selectionVehicle, FareStrategyFactoryExtend via composition/parameters, not a new subclass per ride type

W. Concurrency

Shared mutable state: each Driver.status field, and the GeoIndex's bucket contents as drivers move.

X. Persistence

Two concerns worth separating: (1) ride records — an append-only log of completed/cancelled rides for history, receipts, and disputes, naturally durable and rarely updated after completion; (2) live driver location/status — extremely high write volume, low durability requirement per individual update (only the latest position matters), and a natural fit for an in-memory store (e.g. a fast key-value store) rather than a transactional database. Treating these as one persistence layer with one consistency model is a common mismatch — mentioning the split shows you've thought about write patterns, not just schemas.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDRide/Driver/Rider classes, the state machine, Matching/Fare strategies, the atomic reservation pattern, an in-memory approximation of geo-indexing
HLDA real geohash/quadtree-sharded location service, a message queue for location update fan-out, a matching service that can run across multiple hosts
Distributed SystemsConsistent driver reservation across horizontally scaled matching workers — usually solved with a distributed lock/lease or a single source-of-truth store for driver status, not by replicating in-process locks

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BTrade-off
Driver reservationAtomic CAS on driver statusCoarse lock around the whole matching poolCAS scales under contention; a global lock is simpler but serializes unrelated matches
Surge modelingDecorator over base FareStrategySeparate SurgeFareStrategy duplicating base mathDecorator composes cleanly with any base and future modifiers (promos); a parallel strategy duplicates logic
Geospatial indexGrid/geohash bucketsQuadtreeGrid buckets are simpler to reason about and implement live; quadtrees adapt better to uneven density but cost more implementation time in an interview
Ride lifecycleExplicit guarded state machineFree-form status enum with scattered checksThe state machine centralizes "what's legal next," catching bugs the free-form version would silently allow

AC. Interviewer Evaluation

AD. Interview Simulation

Interviewer: Design something like Uber.
Candidate: To scope this — do you want real geospatial indexing, like a geohash or quadtree, or can I assume a helper that returns nearby drivers and focus on the ride lifecycle and matching/fare logic?
Interviewer: Assume nearby-driver lookup exists; focus on the flow and the matching/fare design.
Candidate: Then I'll separate two independent strategies — a MatchingStrategy that picks among nearby candidates, and a FareStrategy that prices the trip. They vary independently, so I don't want to conflate them into one "ride algorithm" class.
Interviewer: Now add surge pricing.
Candidate: I'd wrap the base FareStrategy with a SurgeFareDecorator rather than writing a second, parallel strategy — surge is a multiplier on top of any base fare, not an alternative way of computing one, so a decorator avoids duplicating the base math.
Interviewer: Two riders request a ride at the same moment, and there's only one available driver nearby. What happens?
Candidate: That's a race condition on the driver's status. I'd make reservation a single atomic compare-and-swap — AVAILABLE → RESERVED — so exactly one request wins; the other falls through and retries the next-nearest candidate. A check-then-set approach would let both requests briefly believe they'd won.
Interviewer: How do you prevent a ride from being marked complete before it's even started?
Candidate: The Ride holds a guarded state machine — each transition checks that the current status is a legal predecessor before applying the change, so completeRide on a still-REQUESTED ride is rejected outright rather than silently accepted.
Interviewer: Last one — how would this scale to millions of drivers across a whole city?
Candidate: At that point the in-process grid index becomes a distributed, sharded geospatial service, driver location updates flow through something like a message queue instead of direct calls, and the atomic reservation needs a single source of truth for driver status across matching workers — but that's an infrastructure/HLD concern, not a change to the class design itself.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
ashishps1/awesome-low-level-design (GitHub)Passenger/Driver/Ride/Location/Payment classes; Singleton RideService orchestrator with addUsers/requestRide/acceptRide/startRide/completeRide/cancelRide; ConcurrentHashMap/ConcurrentLinkedQueue for thread safety; placeholder notifyDrivers/notifyPassenger hooksClean, complete method-level API surface — close to what's proposed in P aboveSingleton service by default; notification hooks are stubs with no real Observer structure; no explicit atomic-reservation discussion despite using concurrent collectionsThe method signatures on the orchestrator class — validated and reused in this document's RideService API
DEV Community — LLD of a Ride-Sharing ApplicationDriver extends User; Haversine-formula distance with a priority queue of nearest available drivers; fare computed inside a "Payment Service"; scaling discussed via microservices + WebSockets/MQTTConcrete, correct use of Haversine + priority queue for candidate ranking — matches the min-heap approach in S aboveNo explicit concurrency mechanism (locks/CAS) despite discussing "handling concurrent requests"; leans on inheritance for Driver/Rider with no discussion of the composition alternative; blends HLD (WebSockets/MQTT) into what's framed as an LLD articleThe Haversine + priority-queue candidate-ranking technique, adopted directly in this document's MatchingStrategy/GeoIndex split
AlgoMaster — Design Ride Hailing Service (partially paywalled)Course structure indicates User/Driver/Rider/Ride/Location/Payment classes with Observer, Strategy, State, and Singleton patterns named explicitlyIf its pattern list is accurate, it's one of few sources naming State for the ride lifecycle, matching this document's choiceFull implementation detail is behind a paywall — cannot verify beyond the outlineCross-check only: its named pattern list (Strategy + Observer + State) lines up with this document's independent choice, which is a useful sanity signal even without seeing the code

AF. Interview Cheat Sheet

ProblemRide-Sharing Service (Uber/Lyft)
DifficultyAdvanced
Core requirementsRequest → match → accept → pickup → in-progress → complete, fare charged, cancellation, ratings
Key clarificationsReal geo-index or assumed lookup? Surge required? Driver can reject? Multiple vehicle types? Payment in scope?
Core entitiesUser, Rider, Driver, Vehicle, Location, Ride, GeoIndex, MatchingStrategy, FareStrategy, NotificationService, RideService
Key relationshipsRide associates Rider/Driver (not composition); SurgeFareDecorator wraps FareStrategy; Ride is a guarded State machine
Key patternsStrategy (matching, fare), Decorator (surge), State (ride lifecycle), Observer (notifications), Facade (RideService)
Most important decisionAtomic driver reservation (CAS), and keeping matching/fare as two independent strategies
Likely-changing behaviorMatching heuristic, fare modifiers, vehicle types, notification channels, geo-index implementation
Concurrency concernDouble-booking race on driver reservation; high-frequency location update contention
Top follow-upsConcurrent reservation race, ride pooling, city-scale matching, driver-goes-offline mid-trip, unresponsive-driver timeout
Common mistakesCheck-then-set reservation; surge as a duplicate parallel strategy; free-form status field; designing HLD infrastructure instead of classes
What NOT to over-engineerDon't build a production quadtree or a real message-broker pipeline — name them as the HLD extension, don't implement them

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

// Ride-Sharing Service (Uber/Lyft) — full LLD implementation with demo.
// Single-file Java translation of the Mermaid class diagram in section J.
import java.util.*;

public class p5 {

    // ---------- Value objects ----------

    static final class Money {
        final long cents;
        private Money(long c) { cents = c; }
        static Money of(double dollars) { return new Money(Math.round(dollars * 100)); }
        Money times(double factor) { return new Money(Math.round(cents * factor)); }
        public String toString() { return String.format("$%.2f", cents / 100.0); }
    }

    /** Immutable geo point. Distance math lives here; storage/indexing is GeoIndex's job. */
    static final class Location {
        final double lat, lng;
        Location(double lat, double lng) { this.lat = lat; this.lng = lng; }
        /** Haversine great-circle distance in kilometers. */
        double distanceTo(Location o) {
            double R = 6371.0;
            double dLat = Math.toRadians(o.lat - lat);
            double dLng = Math.toRadians(o.lng - lng);
            double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                    + Math.cos(Math.toRadians(lat)) * Math.cos(Math.toRadians(o.lat))
                    * Math.sin(dLng / 2) * Math.sin(dLng / 2);
            return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        }
    }

    static final class Vehicle {
        final String make, model, plate, type;
        Vehicle(String make, String model, String plate, String type) {
            this.make = make; this.model = model; this.plate = plate; this.type = type;
        }
    }

    // ---------- Identity hierarchy ----------

    static abstract class User {
        final String id, name, phone;
        double rating = 5.0;
        User(String id, String name, String phone) { this.id = id; this.name = name; this.phone = phone; }
    }

    static final class Rider extends User {
        final String paymentMethod;
        Rider(String id, String name, String phone, String paymentMethod) {
            super(id, name, phone); this.paymentMethod = paymentMethod;
        }
        Ride requestRide(RideService service, Location pickup, Location dropoff) {
            return service.requestRide(this, pickup, dropoff);
        }
    }

    enum DriverStatus { AVAILABLE, RESERVED, ON_TRIP }

    static final class Driver extends User {
        final Vehicle vehicle; // composition: owned by this driver profile
        Location currentLocation; // composition: has no meaning detached from the driver
        DriverStatus status = DriverStatus.AVAILABLE;
        Driver(String id, String name, String phone, Vehicle vehicle, Location location) {
            super(id, name, phone); this.vehicle = vehicle; this.currentLocation = location;
        }
        /** Atomic (in a real system, CAS-guarded) check-and-set: AVAILABLE -> RESERVED. */
        boolean tryReserve() {
            if (status == DriverStatus.AVAILABLE) { status = DriverStatus.RESERVED; return true; }
            return false;
        }
        void release() { status = DriverStatus.AVAILABLE; }
        void markOnTrip() { status = DriverStatus.ON_TRIP; }
        void updateLocation(Location loc) { this.currentLocation = loc; }
        void accept(RideService service, String rideId) { service.acceptRide(rideId, this); }
        void reject(RideService service, String rideId) { service.rejectRide(rideId, this); }
    }

    // ---------- Ride: guarded state machine ----------

    enum RideStatus { REQUESTED, MATCHED, ACCEPTED, ARRIVED, IN_PROGRESS, COMPLETED, CANCELLED, NO_DRIVER_FOUND }

    static final class Ride {
        final String id;
        final Rider rider; // association — rider exists independently of the ride
        final Location pickup, dropoff;
        Driver driver; // association — set once matched
        RideStatus status = RideStatus.REQUESTED;
        Money fare;
        final long requestedAt = System.currentTimeMillis();

        private static final Map<RideStatus, Set<RideStatus>> ALLOWED = new HashMap<>();
        static {
            ALLOWED.put(RideStatus.REQUESTED, EnumSet.of(RideStatus.MATCHED, RideStatus.NO_DRIVER_FOUND, RideStatus.CANCELLED));
            ALLOWED.put(RideStatus.MATCHED, EnumSet.of(RideStatus.ACCEPTED, RideStatus.REQUESTED, RideStatus.CANCELLED));
            ALLOWED.put(RideStatus.ACCEPTED, EnumSet.of(RideStatus.ARRIVED, RideStatus.CANCELLED));
            ALLOWED.put(RideStatus.ARRIVED, EnumSet.of(RideStatus.IN_PROGRESS, RideStatus.CANCELLED));
            ALLOWED.put(RideStatus.IN_PROGRESS, EnumSet.of(RideStatus.COMPLETED));
            ALLOWED.put(RideStatus.COMPLETED, EnumSet.noneOf(RideStatus.class));
            ALLOWED.put(RideStatus.CANCELLED, EnumSet.noneOf(RideStatus.class));
            ALLOWED.put(RideStatus.NO_DRIVER_FOUND, EnumSet.noneOf(RideStatus.class));
        }

        Ride(String id, Rider rider, Location pickup, Location dropoff) {
            this.id = id; this.rider = rider; this.pickup = pickup; this.dropoff = dropoff;
        }

        /** Guarded transition — throws if the target state isn't reachable from the current one. */
        void transitionTo(RideStatus next, Driver withDriver) {
            if (!ALLOWED.get(status).contains(next)) {
                throw new IllegalStateException("Illegal ride transition: " + status + " -> " + next);
            }
            status = next;
            if (withDriver != null) driver = withDriver;
        }
    }

    // ---------- GeoIndex ----------

    /** Spatial lookup of drivers via a coarse lat/lng grid. Ranking is left to MatchingStrategy. */
    static final class GeoIndex {
        private static final double GRID_DEG = 0.05; // ~5km buckets
        private final Map<String, Set<Driver>> buckets = new HashMap<>();
        private final Map<String, String> driverBucket = new HashMap<>();

        private String bucketKey(Location loc) {
            long gx = Math.round(loc.lat / GRID_DEG);
            long gy = Math.round(loc.lng / GRID_DEG);
            return gx + ":" + gy;
        }

        void register(Driver driver) { update(driver, driver.currentLocation); }

        void update(Driver driver, Location loc) {
            String old = driverBucket.get(driver.id);
            if (old != null) buckets.getOrDefault(old, Collections.emptySet()).remove(driver);
            String k = bucketKey(loc);
            buckets.computeIfAbsent(k, x -> new HashSet<>()).add(driver);
            driverBucket.put(driver.id, k);
            driver.updateLocation(loc);
        }

        /** Scans the 3x3 neighborhood of grid cells and filters by real distance. */
        List<Driver> findNearby(Location loc, double radiusKm) {
            long gx = Math.round(loc.lat / GRID_DEG);
            long gy = Math.round(loc.lng / GRID_DEG);
            List<Driver> result = new ArrayList<>();
            for (long dx = -1; dx <= 1; dx++) {
                for (long dy = -1; dy <= 1; dy++) {
                    Set<Driver> cell = buckets.get((gx + dx) + ":" + (gy + dy));
                    if (cell == null) continue;
                    for (Driver d : cell) {
                        if (d.currentLocation.distanceTo(loc) <= radiusKm) result.add(d);
                    }
                }
            }
            return result;
        }
    }

    // ---------- Strategies ----------

    interface MatchingStrategy {
        Optional<Driver> match(Ride ride, List<Driver> candidates);
    }

    static final class NearestDriverStrategy implements MatchingStrategy {
        public Optional<Driver> match(Ride ride, List<Driver> candidates) {
            return candidates.stream()
                    .filter(d -> d.status == DriverStatus.AVAILABLE)
                    .min(Comparator.comparingDouble(d -> d.currentLocation.distanceTo(ride.pickup)));
        }
    }

    interface FareStrategy {
        Money calculate(Ride ride);
    }

    static final class DistanceTimeFareStrategy implements FareStrategy {
        private static final double BASE = 2.50, PER_KM = 1.25, PER_MIN = 0.20, AVG_SPEED_KMH = 30.0;
        public Money calculate(Ride ride) {
            double distanceKm = ride.pickup.distanceTo(ride.dropoff);
            double minutes = (distanceKm / AVG_SPEED_KMH) * 60.0;
            return Money.of(BASE + PER_KM * distanceKm + PER_MIN * minutes);
        }
    }

    /** Decorator: multiplies any wrapped FareStrategy's result during high demand. Not a rival Strategy. */
    static final class SurgeFareDecorator implements FareStrategy {
        private final FareStrategy wrapped;
        private final double multiplier;
        SurgeFareDecorator(FareStrategy wrapped, double multiplier) { this.wrapped = wrapped; this.multiplier = multiplier; }
        public Money calculate(Ride ride) { return wrapped.calculate(ride).times(multiplier); }
    }

    // ---------- Observer ----------

    static final class NotificationService {
        void onRideStatusChanged(Ride ride) {
            System.out.println("[notify] ride " + ride.id + " -> " + ride.status
                    + (ride.driver != null ? " (driver " + ride.driver.name + ")" : ""));
        }
    }

    // ---------- RideService (facade) ----------

    static final class RideService {
        private static final double DEFAULT_RADIUS_KM = 5.0;
        private final GeoIndex geoIndex;
        private final MatchingStrategy matchingStrategy;
        private final FareStrategy fareStrategy;
        private final NotificationService notificationService;
        private final Map<String, Ride> rides = new HashMap<>();
        private int counter = 0;

        RideService(GeoIndex geoIndex, MatchingStrategy matchingStrategy, FareStrategy fareStrategy,
                    NotificationService notificationService) {
            this.geoIndex = geoIndex; this.matchingStrategy = matchingStrategy;
            this.fareStrategy = fareStrategy; this.notificationService = notificationService;
        }

        Ride requestRide(Rider rider, Location pickup, Location dropoff) {
            Ride ride = new Ride("R-" + (++counter), rider, pickup, dropoff);
            rides.put(ride.id, ride);
            attemptMatch(ride, Collections.emptySet());
            return ride;
        }

        /** Core matching loop: rank candidates, reserve atomically, retry on lost race. */
        private void attemptMatch(Ride ride, Set<String> excludedDriverIds) {
            List<Driver> candidates = geoIndex.findNearby(ride.pickup, DEFAULT_RADIUS_KM);
            candidates.removeIf(d -> excludedDriverIds.contains(d.id));
            while (!candidates.isEmpty()) {
                Optional<Driver> chosen = matchingStrategy.match(ride, candidates);
                if (chosen.isEmpty()) break;
                Driver driver = chosen.get();
                if (driver.tryReserve()) { // atomic CAS: AVAILABLE -> RESERVED
                    ride.transitionTo(RideStatus.MATCHED, driver);
                    notificationService.onRideStatusChanged(ride);
                    return;
                }
                candidates.remove(driver); // someone else grabbed it first, retry
            }
            ride.transitionTo(RideStatus.NO_DRIVER_FOUND, null);
            notificationService.onRideStatusChanged(ride);
        }

        void acceptRide(String rideId, Driver driver) {
            Ride ride = rides.get(rideId);
            if (ride.driver != driver) throw new IllegalStateException("Driver mismatch for ride " + rideId);
            ride.transitionTo(RideStatus.ACCEPTED, driver);
            notificationService.onRideStatusChanged(ride);
        }

        /** Rejection releases the driver and re-triggers matching, excluding the driver who just rejected. */
        void rejectRide(String rideId, Driver driver) {
            Ride ride = rides.get(rideId);
            driver.release();
            ride.transitionTo(RideStatus.REQUESTED, null);
            ride.driver = null;
            attemptMatch(ride, Set.of(driver.id));
        }

        void arriveAtPickup(String rideId) {
            Ride ride = rides.get(rideId);
            ride.transitionTo(RideStatus.ARRIVED, null);
        }

        void startRide(String rideId) {
            Ride ride = rides.get(rideId);
            ride.transitionTo(RideStatus.IN_PROGRESS, null); // ARRIVED -> IN_PROGRESS
            ride.driver.markOnTrip();
            notificationService.onRideStatusChanged(ride);
        }

        Ride completeRide(String rideId) {
            Ride ride = rides.get(rideId);
            ride.transitionTo(RideStatus.COMPLETED, null); // throws if not currently IN_PROGRESS
            Money fare = fareStrategy.calculate(ride); // may itself be a decorator chain
            ride.fare = fare;
            System.out.println("[payment] charged " + fare + " to " + ride.rider.name);
            ride.driver.release(); // RESERVED/ON_TRIP -> AVAILABLE
            notificationService.onRideStatusChanged(ride);
            return ride;
        }

        void cancelRide(String rideId, String cancelledBy) {
            Ride ride = rides.get(rideId);
            if (ride.driver != null) ride.driver.release();
            ride.transitionTo(RideStatus.CANCELLED, null);
            System.out.println("[cancel] ride " + rideId + " cancelled by " + cancelledBy);
            notificationService.onRideStatusChanged(ride);
        }
    }

    // ---------- Demo ----------

    public static void main(String[] args) {
        GeoIndex geoIndex = new GeoIndex();
        MatchingStrategy matching = new NearestDriverStrategy();
        FareStrategy fare = new SurgeFareDecorator(new DistanceTimeFareStrategy(), 1.5); // 1.5x surge active
        NotificationService notifications = new NotificationService();
        RideService rideService = new RideService(geoIndex, matching, fare, notifications);

        Driver d1 = new Driver("d1", "Sam", "555-1", new Vehicle("Toyota", "Camry", "ABC-123", "Sedan"), new Location(37.77, -122.42));
        Driver d2 = new Driver("d2", "Jamie", "555-2", new Vehicle("Honda", "Civic", "XYZ-987", "Sedan"), new Location(37.80, -122.41));
        geoIndex.register(d1);
        geoIndex.register(d2);

        Rider rider = new Rider("r1", "Priya", "555-9", "card-ending-4242");
        Ride ride = rider.requestRide(rideService, new Location(37.775, -122.418), new Location(37.79, -122.40));
        System.out.println("After match: status=" + ride.status + " driver=" + (ride.driver != null ? ride.driver.name : "none"));

        ride.driver.accept(rideService, ride.id);
        rideService.arriveAtPickup(ride.id);
        rideService.startRide(ride.id);
        Ride completed = rideService.completeRide(ride.id);
        System.out.println("Final status=" + completed.status + " fare=" + completed.fare);
    }
}