Singleton coordinator: registry of available drivers and active rides.
4. Class diagram
Ride-sharing class model
5. Key interfaces and classes
interface MatchingStrategy { // Strategy — how to pick a driver Optional<Driver> match(List<Driver> available, Ride ride);}final class NearestDriverMatching implements MatchingStrategy { public Optional<Driver> match(List<Driver> available, Ride ride) { return available.stream() .filter(Driver::isAvailable) .min(Comparator.comparingDouble(d -> d.location().distanceTo(ride.pickup()))); }}interface PricingStrategy { // Strategy — fare incl. surge Money fare(Trip trip);}final class SurgePricing implements PricingStrategy { private final double base, perKm, perMin; private final DoubleSupplier surge; // demand/supply multiplier, evaluated now public Money fare(Trip t) { double raw = base + perKm * t.distanceKm() + perMin * t.durationMin(); return Money.of(raw * surge.getAsDouble()); }}
interface RideState { // State — ride lifecycle default void accept(Ride r, Driver d) { illegal(); } default void startTrip(Ride r) { illegal(); } default void endTrip(Ride r) { illegal(); } default void cancel(Ride r) { r.setState(new CancelledState()); } private void illegal() { throw new IllegalStateException("transition not allowed"); }}final class RequestedState implements RideState { public void accept(Ride r, Driver d) { d.setAvailable(false); // driver now committed to one ride r.assignDriver(d); r.setState(new AcceptedState()); }}final class AcceptedState implements RideState { // driver en-route to pickup public void startTrip(Ride r) { r.setState(new InTripState()); }}final class InTripState implements RideState { public void endTrip(Ride r) { Trip trip = r.finalizeTrip(); Money fare = r.pricing().fare(trip); r.paymentService().charge(r.rider(), r.driver(), fare); r.driver().setAvailable(true); r.setState(new CompletedState()); }}
public final class Dispatcher { // Singleton coordinator private static final Dispatcher INSTANCE = new Dispatcher(); public static Dispatcher get() { return INSTANCE; } private final ConcurrentHashMap<String, Driver> available = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Ride> activeRides = new ConcurrentHashMap<>(); private MatchingStrategy matcher = new NearestDriverMatching(); public Ride requestRide(Rider rider, Location pickup, Location drop) { Ride ride = new Ride(rider, pickup, drop); // starts in RequestedState matcher.match(new ArrayList<>(available.values()), ride) .ifPresent(driver -> { ride.accept(driver); // delegates to RideState available.remove(driver.id()); activeRides.put(ride.id(), ride); }); return ride; // may still be unmatched -> retry/expand radius } // Observer fan-out: driver pings location, riders subscribed to the ride are notified. public void updateLocation(Driver driver, Location loc) { driver.setLocation(loc); Ride ride = activeRides.get(driver.currentRideId()); if (ride != null) ride.rider().onLocation(driver, loc); }}
6. Design patterns used
Strategy — MatchingStrategy (nearest / highest-rated / lowest-ETA) and PricingStrategy (flat, surge) vary independently; swapping one never touches the dispatcher or ride.
State — Ride moves through Requested → Accepted → InTrip → Completed, with Cancelled reachable from the early states; each state permits only its legal transitions.
Observer — the rider subscribes to the matched driver's location stream; updateLocation fans out pushes instead of the rider polling.
Singleton — one Dispatcher owns the available-driver registry and active-ride map per deployment (a service in the HLD version).
7. Trade-offs and alternatives
Matching is a Strategy, not a hardcoded scan. Nearest-driver is the default, but lowest-ETA (accounts for traffic) and highest-rated are common variants. At LLD scope a linear scan over available drivers is fine; flag that production uses a geospatial index (geohash/quadtree/S2) — an HLD concern.
Driver assignment must be atomic. Two riders' requests could match the same driver; the dispatcher must claim the driver (remove from available / CAS the availability flag) as part of acceptance, or two rides race onto one driver. This mirrors the parking-spot "find-then-occupy" race.
Surge as a multiplier supplier. Injecting surge as a DoubleSupplier keeps pricing decoupled from how demand/supply is computed; the computation itself is deferred to HLD.
Cancellation policy. Free before acceptance, fee after a driver is en-route — model as a cancellation-policy Strategy rather than branching inside the state.
8. Common follow-up questions
Matching algorithm — swap the linear scan for a geospatial index; discuss ETA-based vs distance-based matching and batching requests to optimize globally.
Pooling (shared rides) — a Ride carries multiple riders with compatible routes; matching becomes a route-overlap problem and pricing splits the fare.
Cancellation policy — tiered fees by state and time elapsed, modeled as a Strategy.
Ratings — both parties rate post-trip; aggregate feeds the highest-rated matching strategy and driver deactivation thresholds.
Disputes — the Trip retains the route/fare breakdown for arbitration; pair with a payment hold/capture flow.