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, cancelRide 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
AVAILABLEtoOFFERED. 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.AVAILABLEsvc = 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