Courses › Low-Level Design › Problem Walkthroughs

Design Ride-Hailing Matching

Ride lifecycles, pluggable matching strategies and preventing double assignment.

"Design a ride-hailing service" and "design food delivery order assignment" are LLD questions about matching: pair requests with available supply (drivers or couriers) according to rules that are expected to change. Interviewers look for a clean lifecycle state machine, a pluggable matching strategy, and correct handling of the race when two requests want the same driver.

Requirements

  • Drivers go online and offline and report their location.
  • A rider requests a ride from A to B. The system finds a suitable driver and offers them the ride. The driver accepts or declines.
  • Ride lifecycle: requested → assigned → driver arriving → in progress → completed, with cancellations from either side.
  • A fare is calculated at the end.
  • Extras: vehicle types, surge pricing, ratings, pooled rides, batching for delivery.

Model

Location            lat, lng
Driver              id, vehicle_type, status (OFFLINE, AVAILABLE, OFFERED, ON_TRIP), location, rating
Rider               id
Ride                id, rider, pickup, dropoff, vehicle_type, status, driver, fare
MatchingStrategy    (interface) rank(ride, candidates) -> list[Driver]
  NearestDriver, BestRatedNearby, LowestEta
PricingStrategy     (interface) fare(ride) -> cents     e.g. BaseFare, Surge
DriverIndex         find_nearby(location, radius, vehicle_type) -> candidates
DispatchService     request_ride, accept, decline, start, complete, cancel

Ride lifecycle

(Requested) -> [driver accepts] -> (Assigned) -> [driver arrives] -> (Arriving) -> [trip starts] -> (In progress) -> [dropoff] -> (Completed)
(Requested) -> [no driver / rider cancels] -> (Cancelled)
(Assigned) -> [cancel] -> (Cancelled)

Enforce the transitions in one place, with a table of allowed (from, to) pairs, so invalid changes like "complete a ride that never started" are rejected.

Matching

  • Find candidates: available drivers of the right vehicle type within a radius. In an interview, a linear scan over the online drivers is fine. Mention a grid or geohash index for scale (see Finding What's Nearby).
  • Rank them with the strategy.
  • Offer the ride to the best one, atomically moving that driver from AVAILABLE to OFFERED. If they decline or don't answer in time, offer it to the next one.
import math
import threading
from enum import Enum


class DriverStatus(Enum):
    OFFLINE = "offline"
    AVAILABLE = "available"
    OFFERED = "offered"
    ON_TRIP = "on_trip"


class Driver:
    def __init__(self, id, vehicle_type, location, rating=5.0):
        self.id, self.vehicle_type, self.location, self.rating = id, vehicle_type, location, rating
        self.status = DriverStatus.AVAILABLE


def distance(a, b):
    return math.dist(a, b)          # stand-in for haversine / road ETA


class NearestDriver:
    def rank(self, pickup, candidates):
        return sorted(candidates, key=lambda d: distance(d.location, pickup))


class BestRatedNearby:
    def __init__(self, radius):
        self.radius = radius

    def rank(self, pickup, candidates):
        close = [d for d in candidates if distance(d.location, pickup) <= self.radius]
        return sorted(close, key=lambda d: (-d.rating, distance(d.location, pickup)))


class DispatchService:
    def __init__(self, strategy, radius=5.0):
        self.strategy, self.radius = strategy, radius
        self.drivers = {}
        self._lock = threading.Lock()

    def add_driver(self, driver):
        self.drivers[driver.id] = driver

    def find_driver(self, pickup, vehicle_type, exclude=()):
        """Atomically reserve the best available driver, or return None."""
        with self._lock:
            candidates = [d for d in self.drivers.values()
                          if d.status is DriverStatus.AVAILABLE and d.vehicle_type == vehicle_type
                          and d.id not in exclude and distance(d.location, pickup) <= self.radius]
            for d in self.strategy.rank(pickup, candidates):
                d.status = DriverStatus.OFFERED      # reserved: no other request can take them
                return d
            return None

    def respond(self, driver, accepted):
        with self._lock:
            driver.status = DriverStatus.ON_TRIP if accepted else DriverStatus.AVAILABLE
svc = DispatchService(NearestDriver())
svc.add_driver(Driver("d1", "sedan", (0, 3)))
svc.add_driver(Driver("d2", "sedan", (0, 1)))
first = svc.find_driver((0, 0), "sedan")
second = svc.find_driver((0, 0), "sedan")       # concurrent request gets the next driver
assert (first.id, second.id) == ("d2", "d1")
svc.respond(first, accepted=False)               # declined: back to available

[!INTERVIEW] The double-assignment race Two riders request at the same moment, and both see the same nearest driver. The fix is to reserve while choosing: pick and mark the driver OFFERED inside one critical section, or with a compare-and-set on the driver's status. Then the second request skips them. The offer has a timeout that returns the driver to AVAILABLE. Raising this race unprompted is a strong signal.

Pricing

Keep pricing separate from matching. PricingStrategy.fare(ride) = base fare + per-km rate × distance + per-minute rate × duration, multiplied by a surge factor from a SurgeProvider that compares demand and supply in the area. New pricing rules, promotions or tolls become new strategies or decorators, without touching dispatch.

Food delivery differences

  • Three parties: the restaurant prepares, the courier picks up, the customer receives. The order state machine has extra states (PREPARING, READY_FOR_PICKUP).
  • Timing: offer the order to a courier so they arrive when the food is ready, not as soon as possible.
  • Batching: one courier can carry two orders from the same restaurant. Matching then works on groups, which is a natural extension of the strategy interface.

Follow-ups

  • Fairness: a strategy that favors drivers who have waited longest, combined with distance.
  • Pooled rides: match a request against trips already in progress whose route detour is small.
  • Driver location updates: high-frequency writes go to the location index, not to the ride objects.
  • Scale: partition by city or region. One dispatcher per region keeps the reservation logic simple. See Design Uber.

[!TIP] Practice Design a Ride-Hailing Service · Food Delivery Order Assignment · Design Patterns