Expert Case Studies

Designing Uber: Real-Time Location, Dispatch, and Ride Matching

Analyze how to design a ride-hailing platform like Uber. Cover geospatial indexing, real-time location tracking, the dispatch/matching service, surge pricing, and the trip state machine.

August 14, 2026
case studyuberride hailinggeospatialreal-timedispatchsurge pricingscale

Why Study Uber?

Uber coordinates millions of concurrent trips across thousands of cities, matching riders with nearby drivers in seconds while both parties are physically moving. Unlike most system design case studies where the hard problem is storage or fan-out, Uber's hard problem is geospatial: finding "who is near me, right now" at massive scale, continuously, under tight latency budgets.

Approach: Don't try to design "all of Uber" at once. Interviewers typically want you to go deep on one of three sub-problems: location indexing, the matching/dispatch algorithm, or the trip lifecycle. Pick a lane, but be ready to connect it to the others.


Requirements Analysis

Functional Requirements

  1. Location Tracking: Continuously track driver (and active rider) locations
  2. Ride Request: Rider requests a trip from pickup to destination
  3. Matching: Find and assign the best nearby available driver
  4. Pricing: Compute fare, including dynamic surge pricing under high demand
  5. Trip Management: Track a trip through its full lifecycle (requested → completed)
  6. Real-Time Updates: Both rider and driver see live location and trip status

Non-Functional Requirements

RequirementTarget
Matching latencyDriver found and notified within a few seconds
Location freshnessDriver position accurate to within a few seconds
AvailabilityMatching must degrade gracefully, never hard-fail
ScaleMillions of active drivers, tracked continuously, globally
ConsistencyA driver must never be double-booked on two trips
Geographic partitioningMatching in one city must be unaffected by load in another

High-Level Architecture


Geospatial Indexing: Geohash vs. S2 Cells

Why Proximity Search Needs an Index

Finding "drivers within 2km of this rider" sounds like a simple query, but a naive approach - scanning every driver row and computing a haversine distance - is O(n) per request. With millions of drivers and thousands of match requests per second, that's untenable. What's needed is a way to turn 2D proximity into something an index (or an in-memory hash map) can answer in near-constant time: a spatial encoding that maps nearby coordinates to nearby (often identical-prefix) keys.

Geohash

Geohash recursively subdivides the world into a grid of rectangular cells, encoding each cell as a base32 string. Every extra character narrows the cell further - 9q8yy is a bigger area than 9q8yyk. Points physically close to each other usually (not always, because of edge effects at cell boundaries) share a common string prefix, which is what makes range queries and prefix lookups useful for proximity search.

Google S2 Cells

S2 projects the Earth's surface onto a cube and recursively subdivides each cube face into a hierarchy of cells, indexed via a Hilbert space-filling curve. The practical advantage over geohash is that S2 cells are closer to uniform in physical area at every latitude and don't suffer from the specific boundary discontinuities geohash has near cell edges (where two physically adjacent points can have very different prefixes) - which matters when doing consistent, predictable "nearest neighbor" ring searches. Uber built its own similar system (originally influenced by S2) for exactly this reason.

Geohash vs. S2 Comparison

AspectGeohashS2 Cells
EncodingBase32 string, lat/lng interleaved bitsHilbert curve index over cube projection
Cell shape uniformityDistorts near poles, rectangularNear-uniform area globally
Boundary behaviorAdjacent points can have very different prefixes at cell edgesBetter neighbor locality via Hilbert curve ordering
SimplicityVery simple to implement, human-readableMore complex, needs a library
Common useRedis GEOADD/GEORADIUS, general-purposeGoogle/Uber-scale ride-hailing, Google Maps internals
💡

Why this matters for matching: Either scheme turns "find nearby drivers" into "look up this cell and its immediate neighbors" - an O(1) hash/index lookup instead of a full scan. The dispatch service keeps an in-memory map of cell ID → set of available driver IDs, updated as location pings arrive, so a match request only ever touches a handful of cells.


Real-Time Location Updates

Why WebSockets, Not Polling

Driver apps maintain a persistent WebSocket (or similar long-lived) connection to a location gateway rather than polling an HTTP endpoint. Polling at a frequency high enough to feel "real-time" (every few seconds) from millions of idle devices wastes enormous connection-setup overhead; a persistent connection lets the server push trip offers and status changes to drivers/riders instantly, and lets drivers stream location cheaply over an already-open channel.

Ping Frequency Tradeoffs

Ping IntervalLocation FreshnessBattery/Bandwidth CostMatching Accuracy
1 secondExcellentHigh - drains battery, high server loadBest, but rarely necessary
4 seconds (typical while driving)GoodModerateHigh - close enough for matching and ETA
15-30 seconds (idle/parked)Reduced, acceptableLowFine - driver isn't moving
Adaptive (speed-based)Best tradeoffScales with actual needHigh

Interview signal: A strong answer doesn't pick one fixed interval - it argues for an adaptive ping rate: faster when the driver's speed/heading indicates active movement (especially mid-trip, where rider-facing ETA accuracy matters most), slower when stationary. This is a direct tradeoff between location freshness, server ingest load, and driver battery life.


Dispatch / Matching Service

Nearest-Available-Driver Algorithm

The matching service doesn't just grab the closest driver by straight-line distance - it ranks candidates by estimated time to pickup (accounting for road network and traffic), and often batches nearby requests together to solve a small local assignment problem rather than greedily matching one-by-one, which reduces total fleet idle time across a busy area.

Matching Timeout and Re-Broadcast

If a driver doesn't accept an offer within a short timeout (typically several seconds), the offer expires and dispatch moves to the next-ranked candidate rather than waiting indefinitely - keeping the rider's wait time bounded. A driver who is offered a trip is provisionally "locked" (reserved) for that offer window so two riders can't simultaneously be matched to the same driver; if the offer times out or is declined, the lock releases and the driver becomes available to the next search.

DecisionChoiceRationale
Ranking metricETA (road-aware), not straight-line distanceStraight-line distance is misleading across rivers, highways, one-ways
Driver lockingShort-lived reservation on offerPrevents double-booking during the accept/decline window
Timeout on offerA few secondsBounds rider wait time; keeps drivers from being "stuck" pending
Search radiusExpanding rings from origin cellBalances match speed against match quality in sparse areas
Batch vs. greedy matchingBatches when request volume is highReduces total idle driving time across many simultaneous requests
💡

Why not just lock the driver forever until they respond? A driver who's gone offline or has a flaky connection would otherwise block that slot indefinitely, denying every other rider a match. A bounded offer window with automatic re-broadcast trades a small amount of matching optimality for guaranteed liveness.


Trip State Machine

Each transition is driven by an explicit event (driver app action, rider app action, or a system timeout) and is written to the trip record, giving both apps a single source of truth for what UI to render. Keeping this as an explicit state machine - rather than a loose set of boolean flags - is what prevents invalid transitions like "completing" a trip that was never marked in-progress, or double-cancelling.

StateMeaningWho Can Trigger Next Transition
RequestedRider submitted a trip requestSystem (begins matching)
MatchingActively searching for a driverSystem (match found or exhausted)
MatchedDriver accepted, not yet en route to pickup confirmedSystem
DriverArrivingDriver en route to pickup locationDriver (arrival), either party (cancel)
InProgressRider picked up, trip underwayDriver (trip end)
CompletedTrip finished, fare finalizedTerminal
CancelledTrip terminated before completionTerminal

Surge Pricing

Supply/Demand Ratio Per Geo-Cell

Surge pricing computes a multiplier per geographic cell (the same cell granularity used for matching) based on the ratio of open ride requests to available drivers in that cell over a short rolling window. A cell with many riders requesting and few available drivers gets a higher multiplier; a balanced or oversupplied cell stays at 1x.

The multiplier is recomputed continuously (every few seconds to a minute) and cached per cell with a short TTL, since demand can shift quickly. It's smoothed and capped to avoid wild spikes from noisy short-term fluctuations (e.g., a temporary lull of two drivers finishing back-to-back trips shouldn't itself trigger extreme surge), and applied to the fare at the moment a rider requests a trip, then locked in for that specific request so the price can't shift after the rider has already committed.

Design ChoiceRationale
Compute per-cell, not per-cityDemand imbalances are hyper-local - a stadium letting out surges independently of the rest of the city
Short TTL cacheSurge state must reflect near-real-time supply/demand, not stale snapshots
Cap and smooth the multiplierPrevents runaway pricing from brief, noisy fluctuations
Lock price at request timeRider needs a firm quote before confirming; price shouldn't change mid-request

Interview tip: Surge pricing is really a load-balancing signal disguised as a pricing feature - a higher price both rations scarce driver supply toward the riders who value it most and incentivizes nearby idle drivers to reposition into the high-demand cell. Framing it that way (not just "dynamic pricing") tends to land well in interviews.


Database Schema

Drivers Table

sql
CREATE TABLE drivers (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    vehicle_type VARCHAR(20) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'offline',
    -- offline | available | offered | on_trip
    rating DECIMAL(3,2) DEFAULT 5.00,
    current_cell_id VARCHAR(20),
    last_ping_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT NOW()
);
 
CREATE INDEX idx_status_cell ON drivers(status, current_cell_id);

Trips Table

sql
CREATE TABLE trips (
    id BIGINT PRIMARY KEY,
    rider_id BIGINT NOT NULL,
    driver_id BIGINT,
    state VARCHAR(20) NOT NULL DEFAULT 'requested',
    -- requested | matching | matched | driver_arriving | in_progress | completed | cancelled
    pickup_lat DOUBLE PRECISION NOT NULL,
    pickup_lng DOUBLE PRECISION NOT NULL,
    dropoff_lat DOUBLE PRECISION NOT NULL,
    dropoff_lng DOUBLE PRECISION NOT NULL,
    surge_multiplier DECIMAL(4,2) DEFAULT 1.00,
    fare_estimate_cents INT,
    fare_final_cents INT,
    requested_at TIMESTAMP DEFAULT NOW(),
    matched_at TIMESTAMP,
    started_at TIMESTAMP,
    completed_at TIMESTAMP
);
 
CREATE INDEX idx_rider_id ON trips(rider_id, requested_at DESC);
CREATE INDEX idx_driver_id ON trips(driver_id, requested_at DESC);
CREATE INDEX idx_state ON trips(state);

Driver Locations (Time-Series-ish Table)

sql
CREATE TABLE driver_locations (
    driver_id BIGINT NOT NULL,
    lat DOUBLE PRECISION NOT NULL,
    lng DOUBLE PRECISION NOT NULL,
    cell_id VARCHAR(20) NOT NULL,
    heading SMALLINT,
    speed_mps DECIMAL(5,2),
    recorded_at TIMESTAMP NOT NULL,
    PRIMARY KEY (driver_id, recorded_at)
);
 
CREATE INDEX idx_cell_recent ON driver_locations(cell_id, recorded_at DESC);
💡

Why driver_locations isn't just one row per driver: Keeping a short rolling history (partitioned/pruned aggressively, e.g. last few minutes retained, older rows dropped or archived) enables heading/speed derivation, trip playback/ETA smoothing, and auditing - while the live matching path reads from an in-memory cell index, not this table directly, since a disk-backed table can't sustain sub-second lookups at this write volume.


End-to-End Ride Request Flow


Scaling Challenges & Solutions

ChallengeSolution
Naive proximity search doesn't scaleGeohash/S2 cell indexing turns lookups into O(1) hash/index reads
Millions of drivers pinging location continuouslyPersistent WebSocket connections + adaptive ping frequency
Preventing double-booked driversShort-lived reservation lock during the offer/accept window
Slow-to-respond or offline drivers stalling matchesBounded offer timeout with automatic re-broadcast to next candidate
Straight-line distance misleads matchingRank candidates by road-aware ETA, not raw distance
Sudden local demand spikes (events, weather)Per-cell surge multiplier recomputed on a short rolling window
Invalid or inconsistent trip status transitionsExplicit trip state machine enforced server-side
High-frequency location writes overwhelming a single DBCell index kept in-memory for live matching; disk-backed table used only for history/audit
City-specific load spikes affecting other citiesGeographic partitioning of matching/dispatch by region

Key Takeaways

  1. Spatial indexing is the foundation: Geohash or S2 cells turn "who's nearby" from an O(n) scan into an O(1) lookup - this is the single most important design decision in the whole system.
  2. Persistent connections beat polling for real-time state: WebSockets let the server push offers and status instantly and let clients stream location cheaply.
  3. Matching is a ranked, time-bounded search, not a single lookup: ETA-based ranking, offer timeouts, and re-broadcast keep the system both accurate and live.
  4. Model the trip lifecycle explicitly: A state machine enforced server-side prevents an entire class of "impossible state" bugs.
  5. Surge pricing is a supply/demand signal, not just a price: It rations scarce drivers and pulls in idle supply from nearby cells.
  6. Separate the hot path from the historical record: Live matching reads an in-memory index; the durable location table serves history, auditing, and analytics.

Interview tip: When asked to design Uber, start by naming the geospatial indexing choice explicitly (geohash vs. S2, and why) before diagramming services - it signals you understand the actual bottleneck, rather than treating this like a generic CRUD system with a map on top.


Follow-Up Questions to Consider

  1. How would you handle a driver going offline mid-trip (app crash, connectivity loss, or the phone dying)?
  2. How would you design multi-stop or shared/pooled rides, where a driver serves more than one rider concurrently?
  3. How would you extend the matching algorithm to account for driver preferences (e.g., avoiding certain areas, ending a shift soon)?
  4. How would you detect and prevent GPS spoofing or fraudulent location pings from drivers trying to manipulate surge zones?
  5. How would you design the ETA prediction system itself - the piece that ranks candidates by predicted arrival time?
💡

Real Uber trivia: Uber originally built its own H3 hexagonal grid system (open-sourced in 2018) for spatial indexing and analytics, favoring hexagons over the square/rectangular cells of geohash because hexagonal grids have uniform adjacency - every neighboring cell is equidistant from the center, which avoids distance-distortion artifacts that square grids have at their corners versus their edges.