Low-Level Design: Ride-Sharing Dispatch System — Matching, Routing, and Surge Pricing

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.

Scroll to Top