Vetora logo
Hard8 componentsInterview: High

Ride-Hailing (Uber/Lyft)

A ride-hailing platform like Uber matches riders with nearby drivers in under 10 seconds by querying a Redis geohash index of 100K+ GPS updates/s, computing road-network ETAs, and ranking candidates by a composite score. This 8-component architecture handles surge pricing via H3 hexagonal cells, real-time trip tracking over WebSocket, and metered fare calculation across multiple regions at 99.95% availability.

GeoReal-TimeMatching
Problem Statement

Ride-hailing is one of the most challenging system design problems because it combines real-time geospatial processing, dynamic pricing algorithms, and two-sided marketplace matching into a single system. Building a platform like Uber or Lyft requires solving the fundamental problem of connecting riders with nearby available drivers within seconds, while continuously tracking the location of millions of moving vehicles and computing accurate arrival time estimates.

At Uber's scale, the system processes over 15 million trips per day, tracks the GPS coordinates of millions of active drivers updated every 4 seconds, and must match riders with drivers in under 10 seconds to maintain a good user experience. The geospatial matching problem is computationally intensive: for each ride request, the system must search a dynamic set of moving points (drivers) within a geographic radius, rank candidates by ETA, and handle the race condition where multiple riders may request the same driver simultaneously.

Surge pricing adds another layer of complexity. The system must monitor supply (available drivers) and demand (ride requests) in real-time across geographic zones, compute dynamic price multipliers, and communicate pricing transparently to riders before they confirm. Surge pricing must respond to demand changes within minutes to effectively redistribute supply and prevent service degradation in high-demand areas.

This template models the complete ride-hailing architecture: API gateway, trip orchestrator, driver location service with geospatial indexing, matching engine with ETA computation, surge pricing service, payment service, and real-time notification hub. The simulation demonstrates how geospatial index choice affects matching latency and how surge pricing parameters influence driver redistribution during demand spikes.

Architecture Overview

## How the Geospatial Index Tracks Driver Locations in Real Time

The ride-hailing architecture is organized around two real-time data flows: driver location updates and trip lifecycle management. The Driver Location Service ingests GPS updates from driver apps at 4-second intervals, processing over 100K updates per second at scale. Each update is written to a geospatial index — a geohash grid backed by Redis GEOADD commands — that enables efficient nearest-neighbor queries. The index is maintained in-memory for sub-millisecond query performance, with each driver entry consuming approximately 100 bytes of Redis memory. Dead reckoning on the client interpolates positions between 4-second GPS updates for smooth map rendering.

## Matching Engine and ETA-Based Driver Ranking

When a rider requests a trip, the Trip Orchestrator queries the Matching Engine, which performs a GEORADIUS search for available drivers within a configurable radius (typically starting at 3km and expanding outward). The Matching Engine computes ETAs for candidate drivers using road-network routing (not straight-line distance) and ranks them by a composite scoring function: 40% ETA weight, 30% driver rating, 20% ML-predicted acceptance probability, and 10% vehicle type match quality. The top candidate receives the trip offer via WebSocket with a 15-second accept/decline timeout; if they decline or timeout, the next candidate is offered sequentially.

## Surge Pricing with Exponential Smoothing

The Surge Pricing Service continuously monitors a grid of geographic zones using hexagonal H3 cells, computing the ratio of active ride requests to available drivers in each cell. When demand exceeds supply beyond a threshold, the multiplier increases in discrete steps. The pricing model uses exponential smoothing to prevent oscillation — multipliers ramp up quickly during demand spikes but decay slowly to avoid the "surge cliff" problem where all drivers move to a surge zone simultaneously. The surge multiplier is locked at booking time so riders are not surprised by price changes during their trip.

## Trip Lifecycle State Machine and Fare Calculation

Once matched, the Trip Orchestrator manages the trip lifecycle through states: MATCHED, DRIVER_EN_ROUTE, ARRIVED, IN_PROGRESS, COMPLETED. Real-time location updates from the driver are pushed to the rider's app via WebSocket, enabling live tracking on the map. At trip completion, the Payment Service calculates the fare using metered distance from the GPS trace and elapsed time, multiplied by the base rate and any surge multiplier captured at booking. The fare is processed, split between the driver (typically 75-80%) and the platform, and receipts are pushed to both parties.

Architecture Preview
Loading architecture preview...
Request Flow — Ride Request to Driver Match

The ride-hailing request flow is a real-time matching pipeline that must find the best available driver within seconds. The system continuously ingests GPS updates from every active driver (every 4 seconds) and maintains an in-memory geospatial index for sub-millisecond proximity queries. When a rider requests a ride, the Matching Engine queries this index for nearby drivers, computes ETAs using road-network routing, and ranks candidates by a scoring function.

The Surge Pricing Service operates as a parallel data pipeline. It monitors supply-demand ratios across H3 hexagonal cells (a geographic grid system). When demand exceeds supply in a cell, the surge multiplier increases. The pricing uses exponential smoothing to prevent oscillation — sudden spikes are damped rather than immediately reflected in fares.

The trip lifecycle is a state machine managed by the Trip Orchestrator: MATCHING → DRIVER_OFFERED → DRIVER_EN_ROUTE → ARRIVED → IN_PROGRESS → COMPLETED. Real-time location updates are pushed to the rider via WebSocket throughout the trip. The Payment Service calculates the final fare using metered distance and time plus the surge multiplier captured at booking time (locked in, not re-evaluated during the ride).

Loading diagram...

Step-by-Step Walkthrough

  1. 1Drivers continuously send GPS updates every 4 seconds to the Geospatial Index (Redis with geohash). Each update overwrites the driver's position in the index, maintaining a real-time map of all active drivers (~0.5ms per update).
  2. 2Rider submits a ride request with pickup location and destination. The API Gateway forwards to the Trip Orchestrator, which creates a new trip record with status MATCHING.
  3. 3The Trip Orchestrator queries the Surge Pricing Service for the current multiplier at the rider's H3 cell. The multiplier is locked in at this point — it won't change even if surge conditions shift during the trip.
  4. 4The Trip Orchestrator sends a match request to the Matching Engine with the pickup location, search radius (starting at 3km), and vehicle type filter.
  5. 5The Matching Engine performs a GEORADIUS query on the Redis geospatial index to find all available drivers within the radius. This is a sub-millisecond operation even with millions of driver records.
  6. 6For each nearby driver, the Matching Engine computes the road-network ETA (not straight-line distance) and calculates a composite score: 40% ETA weight, 30% driver rating, 20% acceptance probability, 10% vehicle match quality.
  7. 7The Trip Orchestrator sends a ride offer to the top-ranked driver via WebSocket with a 15-second accept/decline timeout. If the driver declines or times out, the next candidate is offered. The radius expands (3km → 5km → 8km) after exhausting all candidates.
  8. 8On acceptance, the Trip Orchestrator transitions the trip to DRIVER_EN_ROUTE and pushes the driver's profile, vehicle details, ETA, and surge multiplier to the rider via WebSocket. Real-time driver location is streamed to the rider throughout the trip.
  9. 9Upon trip completion, the Payment Service calculates the fare using metered distance (GPS trace), elapsed time, base rate, and the surge multiplier captured at booking. The fare breakdown is pushed to both rider and driver.

Pseudocode

// Matching Engine — find and rank best driver
async function matchDriver(pickup, radius, vehicleType):
    // 1. Geo query — all drivers within radius (~0.5ms)
    candidates = await redis.georadius(
        "driver:locations", pickup.lng, pickup.lat,
        radius, "km", { withCoord: true, withDist: true, count: 20 }
    )

    // 2. Filter by availability and vehicle type
    available = candidates.filter(d =>
        d.status === "available" && d.vehicleType === vehicleType
    )

    // 3. Compute ETAs via routing service (batch, ~50ms)
    etas = await routingService.batchETA(
        available.map(d => ({ from: d.location, to: pickup }))
    )

    // 4. Score and rank
    ranked = available.map((driver, i) => ({
        driverId: driver.id,
        eta: etas[i],
        score: 0.4 * normalize(etas[i])           // Lower ETA = higher score
             + 0.3 * driver.rating / 5.0           // Higher rating = higher
             + 0.2 * driver.acceptanceProbability   // ML-predicted
             + 0.1 * vehicleMatchScore(driver)      // Exact match bonus
    })).sort((a, b) => b.score - a.score)

    return ranked

// Trip Orchestrator — state machine
async function requestRide(riderId, pickup, destination):
    surge = await surgeService.getMultiplier(toH3Cell(pickup))
    trip = await db.createTrip({
        riderId, pickup, destination,
        surgeMultiplier: surge.multiplier,  // locked at booking
        status: "MATCHING"
    })

    candidates = await matchingEngine.match(pickup, 3_km, "standard")

    for candidate in candidates:
        offered = await pushOffer(candidate.driverId, trip, timeout: 15_s)
        if offered.accepted:
            trip.driverId = candidate.driverId
            trip.status = "DRIVER_EN_ROUTE"
            await notifyRider(riderId, { driver: candidate, eta: candidate.eta })
            return trip

    // No driver found — expand radius and retry
    return requestRide(riderId, pickup, destination, radius: 5_km)
Key Design Decisions
Geospatial Index

Choice

Geohash grid with Redis GEOSEARCH, backed by H3 hexagonal cells for surge zones

Rationale

Geohash provides O(1) lookups for drivers within a grid cell and efficient range queries by searching adjacent cells. Redis GEOSEARCH supports radius queries natively with sub-millisecond latency. H3 hexagonal cells are used for surge pricing because hexagons provide uniform adjacency (every neighbor is equidistant), unlike rectangular grids which have corner-adjacency distortion.

Matching Algorithm

Choice

Greedy nearest-available with ETA-based ranking and sequential offers

Rationale

Optimal matching (e.g., Hungarian algorithm across all pending requests and drivers) is computationally prohibitive at scale and introduces unacceptable latency. Greedy matching with ETA ranking provides near-optimal results in practice because ride requests arrive continuously and the matching window is short. Sequential offers (one driver at a time) are simpler than batch assignment and handle the real-world constraint that drivers may decline.

Location Update Frequency

Choice

4-second GPS intervals with dead reckoning interpolation

Rationale

More frequent updates (1-second) increase bandwidth and server load proportionally without meaningfully improving matching accuracy. Less frequent updates (10-second) create visible 'jumping' on the rider's map. 4-second intervals balance accuracy against resource consumption. Client-side dead reckoning interpolates position between updates for smooth map rendering.

Surge Pricing Model

Choice

Exponential smoothing with asymmetric ramp-up/decay rates

Rationale

Instantaneous surge multipliers based on current supply/demand ratios cause oscillation: surge goes up, drivers flood the zone, surge drops, drivers leave, surge goes up again. Exponential smoothing dampens this feedback loop. Asymmetric rates (fast ramp-up, slow decay) ensure riders see fair prices during genuine demand spikes while avoiding the cliff effect.

Scale & Performance

Target RPS

100K location updates/s

Latency (p99)

<10s (rider-to-driver match)

Storage

~2 TB/year (trip + location data)

Availability

99.95%

This template is for educational and illustration purposes only. It may not represent the optimal production design for this problem. Real-world systems involve additional considerations (compliance, specific cloud provider constraints, organizational requirements) not captured here. Use this as a starting point for discussion, not as a production blueprint.

Frequently Asked Questions
How does Uber match riders with drivers in real-time?

Uber uses a geospatial index (geohash grid) to find available drivers near the rider's location. When a ride is requested, the matching engine queries drivers within an expanding radius, computes estimated time of arrival (ETA) for each candidate using road-network routing, and ranks them by a scoring function. The top-ranked driver receives the offer first; if they decline, the next candidate is offered. The entire matching process completes in under 10 seconds.

How does surge pricing work in ride-hailing systems?

Surge pricing monitors real-time supply (available drivers) and demand (ride requests) across geographic zones. When demand exceeds supply beyond a threshold, a price multiplier is applied. The multiplier increases in steps based on the supply-demand ratio. Modern implementations use machine learning to predict demand surges before they occur and pre-position drivers, reducing the need for aggressive surge multipliers.

What geospatial data structure is best for tracking driver locations?

The most common choices are geohash grids (used with Redis GEOSEARCH), QuadTrees, and R-Trees. Geohash grids with Redis are the most practical for production systems because they provide O(1) cell lookups, efficient radius queries via adjacent cell scanning, and built-in support in Redis. QuadTrees offer better spatial partitioning for non-uniform distributions but require custom implementation. R-Trees are optimal for range queries but have higher insertion costs.

How do you compute ETA accurately in a ride-hailing system?

Accurate ETA computation uses road-network routing (not straight-line distance) with real-time traffic data. The routing engine (e.g., OSRM, Google Directions API) computes shortest-time paths considering one-way streets, turn restrictions, and current traffic conditions. ETAs are calibrated using historical trip data — comparing predicted vs. actual arrival times — and ML models adjust for factors like time of day, weather, and local events.

How does a ride-hailing system handle driver location updates at scale?

Each active driver sends GPS coordinates every 4 seconds — at Uber's scale, this generates millions of updates per second. Updates are ingested through a message queue (Kafka) partitioned by geographic region, processed by a stream processor that updates the in-memory geospatial index, and persisted to a time-series database for historical analysis. The system uses batching and compression to reduce network bandwidth, and dead reckoning on the client to interpolate positions between updates.

How would you explain the geohash vs. QuadTree trade-off for driver indexing to an interviewer?

Geohash encodes latitude/longitude into a string prefix where shared prefixes indicate proximity, enabling Redis-native GEOSEARCH queries with O(1) cell lookups and sub-millisecond response times. QuadTrees recursively subdivide space into quadrants, adapting cell density to driver concentration, which avoids the geohash problem of uniform cell sizes in areas with non-uniform driver distribution. The trade-off is simplicity versus precision: geohash with Redis requires zero custom code and scales horizontally, but may over-scan sparse regions. QuadTrees require custom in-memory implementation but produce tighter candidate sets. At Uber scale, geohash with Redis is the standard choice because operational simplicity outweighs the marginal efficiency gain of QuadTrees.

How would you handle the race condition where two riders request the same driver simultaneously?

The race condition is resolved through optimistic locking on the driver's availability status. When the Matching Engine selects a driver, the Trip Orchestrator performs an atomic compare-and-swap operation: set status to 'offered' only if current status is 'available'. If two orchestrator instances attempt this simultaneously, exactly one succeeds and the other receives a conflict, causing it to fall back to the next-ranked candidate. This is implemented as a Redis SETNX or a Postgres UPDATE WHERE status='available' returning the affected row count. The 15-second offer timeout ensures that a driver who is offered but does not respond eventually returns to the available pool.

How would you estimate the infrastructure cost of the geospatial index for a ride-hailing system in an interview?

Start with active drivers: 500,000 drivers sending GPS updates every 4 seconds produces 125,000 writes/s. Each Redis GEOADD stores a sorted set member with longitude, latitude, and driver ID, consuming roughly 100 bytes per entry. For 500K drivers, the total Redis memory is approximately 50 MB, trivially fitting on a single instance. The compute cost is dominated by GEORADIUS queries during ride matching: at 10,000 ride requests per minute, each scanning roughly 20 candidates in a 3 km radius, the query load is manageable at under 5% CPU on a modern Redis instance. The real cost driver is the routing/ETA computation, which requires a dedicated service with 50-100ms per batch call to a road-network engine like OSRM.

Related Templates

Discussion

Sign in to join the discussion.

Ready to design your own Ride-Hailing (Uber/Lyft)?

Open the simulator, place components on the canvas, wire them up, and run a traffic simulation to see how your architecture performs under real load.

Open Simulator