Core Entities
Driver: driver_id, name, vehicle_type (ECONOMY/COMFORT/XL), status (OFFLINE, AVAILABLE, ON_TRIP), current_location (POINT: lat/lng), last_location_update, rating, acceptance_rate. Rider: rider_id, name, payment_method_id, rating, created_at. Trip: trip_id, rider_id, driver_id, status (REQUESTED, MATCHING, DRIVER_ASSIGNED, DRIVER_EN_ROUTE, IN_PROGRESS, COMPLETED, CANCELLED), pickup_location (POINT), dropoff_location (POINT), vehicle_type, estimated_pickup_seconds, estimated_duration_seconds, base_fare, surge_multiplier, final_fare, requested_at, matched_at, pickup_at, dropoff_at. DriverLocation: driver_id, location (POINT), heading, speed, updated_at. (Separate high-write table, updated every 4 seconds.)
Driver Location Tracking
class DriverLocationService:
def update_location(self, driver_id: int, lat: float, lng: float,
heading: float, speed: float):
# Store in Redis geospatial index for fast nearby queries
self.redis.geoadd("driver_locations", lng, lat, driver_id)
self.redis.hset(f"driver:{driver_id}", mapping={
"lat": lat, "lng": lng, "heading": heading,
"speed": speed, "updated_at": time.time()
})
# Also write to DB for trip history and analytics (async)
self.kafka.produce("driver-locations", {
"driver_id": driver_id, "lat": lat, "lng": lng,
"heading": heading, "ts": time.time()
})
def get_nearby_drivers(self, lat: float, lng: float,
radius_km: float,
vehicle_type: str) -> list[dict]:
# GEORADIUS returns drivers within radius sorted by distance
nearby = self.redis.georadius(
"driver_locations", lng, lat, radius_km, "km",
withcoord=True, withdist=True, sort="ASC", count=20
)
result = []
for driver_id, dist, coords in nearby:
meta = self.redis.hgetall(f"driver:{driver_id}")
if meta.get("status") == "AVAILABLE" and
meta.get("vehicle_type") == vehicle_type:
result.append({
"driver_id": driver_id,
"distance_km": dist,
"lat": float(meta["lat"]),
"lng": float(meta["lng"])
})
return result
Matching Algorithm
class DispatchService:
def match_driver(self, trip: Trip) -> Optional[int]:
nearby = self.location_svc.get_nearby_drivers(
trip.pickup_lat, trip.pickup_lng,
radius_km=5.0, vehicle_type=trip.vehicle_type
)
if not nearby:
return None
# Score drivers: weight by ETA (primary) and acceptance rate (secondary)
for d in nearby:
eta = self._estimate_eta(d["lat"], d["lng"],
trip.pickup_lat, trip.pickup_lng)
d["score"] = eta - (d["acceptance_rate"] * 30) # lower = better
nearby.sort(key=lambda x: x["score"])
# Offer to top N drivers sequentially (waterfall dispatch)
for candidate in nearby[:3]:
response = self._offer_trip(candidate["driver_id"], trip, timeout=15)
if response == "ACCEPT":
return candidate["driver_id"]
elif response == "REJECT":
# Mark as rejected for this trip to avoid re-offering
self.redis.setex(
f"trip:{trip.trip_id}:rejected:{candidate['driver_id']}",
300, "1"
)
return None # no driver accepted; retry or cancel
Surge Pricing
class SurgePricingService:
def compute_surge(self, lat: float, lng: float,
vehicle_type: str) -> float:
# Define geographic cell (H3 hexagonal grid or simple grid)
cell = self._lat_lng_to_cell(lat, lng, resolution=9)
# Count available drivers and pending requests in this cell
available_drivers = self.redis.get(f"supply:{cell}:{vehicle_type}") or 0
pending_requests = self.redis.get(f"demand:{cell}:{vehicle_type}") or 0
available_drivers = int(available_drivers)
pending_requests = int(pending_requests)
if available_drivers == 0:
ratio = float("inf")
else:
ratio = pending_requests / available_drivers
# Surge multiplier table
if ratio < 0.5: return 1.0 # excess supply
if ratio < 1.0: return 1.2
if ratio < 2.0: return 1.5
if ratio < 3.0: return 2.0
return 2.5 # severe shortage
def update_supply_demand(self, cell: str, vehicle_type: str,
delta_supply: int, delta_demand: int):
pipe = self.redis.pipeline()
pipe.incrby(f"supply:{cell}:{vehicle_type}", delta_supply)
pipe.incrby(f"demand:{cell}:{vehicle_type}", delta_demand)
pipe.expire(f"supply:{cell}:{vehicle_type}", 300)
pipe.expire(f"demand:{cell}:{vehicle_type}", 300)
pipe.execute()
Trip Lifecycle and Fare Calculation
Trip state transitions: REQUESTED (rider submits) -> MATCHING (dispatch in progress) -> DRIVER_ASSIGNED (driver accepted) -> DRIVER_EN_ROUTE (driver heading to pickup) -> IN_PROGRESS (trip started, rider in car) -> COMPLETED (dropoff) or CANCELLED. Fare calculation: base_fare = base_amount + (per_km_rate * distance_km) + (per_minute_rate * duration_minutes). Final fare = base_fare * surge_multiplier. Minimum fare enforced. Surge multiplier snapshotted at trip request time – rider sees and accepts it. Cancellation fee: if rider cancels after driver is en route for > 2 minutes, charge a flat fee. Rating: both driver and rider rate after completion; ratings are exponential moving average to prevent gaming by single reviews.
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How does a ride-sharing app find nearby available drivers?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store driver locations in a Redis geospatial index using GEOADD (lng, lat, driver_id). When a ride is requested, call GEORADIUS to find all drivers within a radius (e.g., 5km), sorted by distance, limited to the first 20 results. Filter by driver status (AVAILABLE) and vehicle type. Redis GEORADIUS runs in O(N+log(M)) where N is results and M is total drivers – very fast. Driver locations are updated every 4 seconds via a background heartbeat. Stale drivers (no update for 30+ seconds) are filtered out using a last_update timestamp stored in a Redis hash alongside each driver.”}},{“@type”:”Question”,”name”:”How does waterfall dispatch work in ride-sharing matching?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Waterfall dispatch offers the trip to one driver at a time, sequentially. Score the nearby drivers by ETA to pickup (primary) and acceptance rate (secondary). Offer the trip to the highest-scoring driver with a 15-second timeout. If they accept: match made. If they reject or timeout: offer to the next candidate. Cap at 3 offers per trip cycle; if all 3 decline, retry with fresh candidates or cancel after N failed cycles. This differs from broadcast dispatch (offer to all simultaneously) which causes confusion when multiple drivers accept. Waterfall is fairer and simpler to implement correctly.”}},{“@type”:”Question”,”name”:”How does surge pricing work in a ride-sharing system?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Divide the city into geographic cells (H3 hexagonal grid). For each cell, maintain real-time counts of available drivers (supply) and pending ride requests (demand) in Redis with 5-minute TTL. Compute demand/supply ratio. Map ratio to multiplier: ratio < 0.5 -> 1.0x, ratio 1-2 -> 1.5x, ratio > 3 -> 2.5x. The multiplier is shown to the rider before confirming. The multiplier is snapshotted at request time and locked in – the rider pays the price shown even if surge changes during the trip. Supply/demand counters are updated on driver status changes and trip requests.”}},{“@type”:”Question”,”name”:”How do you track driver locations at scale for a ride-sharing platform?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”High-write workload: 1M active drivers updating every 4 seconds = 250K writes/second. Architecture: driver app sends location to a location update service. Service writes to Redis GEOADD (for spatial queries) and a Redis hash (for metadata like status, heading, speed). Asynchronously, publishes to Kafka for persistence and analytics. Kafka consumers write to a DriverLocation table for trip history. Do not write every location update to the primary DB – it cannot sustain 250K writes/second. Redis is the hot path; Kafka/DB is the cold path.”}},{“@type”:”Question”,”name”:”How do you calculate the fare for a ride-sharing trip?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Fare components: base_amount (flat fee to start) + distance_rate * distance_km + time_rate * duration_minutes. Minimum fare enforced. Apply surge_multiplier (snapshotted at request time). Fare calculation uses the actual trip distance from GPS traces (not straight-line), which accounts for traffic detours. Distance is computed from the ordered sequence of GPS points recorded during the trip. Cancellation fee: if rider cancels after driver is en route > 2 minutes, charge a flat fee. Airport and toll surcharges added if applicable. All components stored on the Trip record for transparency and dispute resolution.”}}]}
Uber system design interviews cover driver dispatch and matching. See common questions for Uber interview: ride dispatch and driver matching system design.
Lyft system design rounds cover dispatch and matching algorithms. Review design patterns for Lyft interview: ride-sharing dispatch system design.
DoorDash system design covers delivery dispatch and driver assignment. See patterns for DoorDash interview: delivery dispatch and assignment system design.