System design · Geospatial indexing, matching
How to design Uber
A ride-hailing app looks like a map with cars on it. Underneath, it is a write-heavy system: every active driver reports their position every few seconds, and the system must answer "which drivers are near this rider?" in milliseconds, then hand exactly one of them the trip - even when two riders ask for the same car at the same moment.
Updated · 7 min read
Requirements
Agree the scope first. Ride-hailing touches payments, maps, fraud and support; interviewers want the core loop of request, match and ride.
- Functional: drivers send their location while online; riders see nearby cars and request a ride; the system matches a driver, who accepts or declines.
- Functional: track the trip from pickup to drop-off; show an ETA and a price before the rider confirms, including surge pricing when demand is high.
- Non-functional: nearby-driver search in well under a second; a driver is never assigned to two trips at once; a rider is never charged for a trip that didn't happen.
- Non-functional: location data can be slightly stale or lost - the next update replaces it - but trip and payment data must be durable.
Capacity estimates
These are assumptions, stated out loud, not real figures for Uber. The point is the order of magnitude, which decides the architecture.
| Quantity | Assumption | Result |
|---|---|---|
| Location updates | 2M drivers online at peak, one update every 4 seconds | ≈ 500,000 writes/s at peak |
| Ride requests | 20M trips a day, 3× peak | ≈ 230/s average, ≈ 700/s at peak |
| Nearby searches | riders with the app open refresh every 5 seconds, 1M at peak | ≈ 200,000 reads/s at peak |
| Location history | ≈ 100 bytes per update, 1M drivers online on average | ≈ 25 MB/s ≈ 2 TB a day, if kept |
Three conclusions. Location writes outnumber trip requests by almost a thousand to one, so the location path and the trip path must be separate systems. The current position of every driver is small - a few hundred megabytes - and fits in memory. And location history is large but only needed for analytics and disputes, so it can go to cheap append-only storage off the hot path.
API design
Drivers stream location over a long-lived connection because they send so often. Riders use plain HTTP for requests, and a push channel to follow the trip.
WebSocket /driver/ws
→ { "type": "location", "lat": 37.7749, "lng": -122.4194, "heading": 90, "ts": 1791273600 }
← { "type": "offer", "trip_id": "t_881", "pickup": { ... }, "expires_in": 15 }
→ { "type": "accept", "trip_id": "t_881" }
GET /api/drivers/nearby?lat=37.77&lng=-122.42
→ 200 OK { "drivers": [ { "lat": ..., "lng": ..., "heading": ... } ] }
POST /api/trips
{ "pickup": { ... }, "dropoff": { ... }, "quote_id": "q_52", "idempotency_key": "r-7c1a" }
→ 201 Created { "trip_id": "t_881", "status": "matching" }The quote_id pins the price the rider saw, so surge changing a second later doesn't change the bill. The idempotency_key stops a retried request from creating two trips.
Data model
Two very different stores. Live driver positions live in memory, keyed by geospatial cell. Trips live in a durable database with transactions, sharded by city or region.
driver_locations (in memory, e.g. Redis)
cell:{cell_id} set of driver_ids in that cell
driver:{id} lat, lng, heading, status, cell_id, updated_at (TTL ≈ 30s)
trips (durable, sharded by region)
trip_id bigint primary key
rider_id bigint
driver_id bigint null until matched
status enum requested | matching | accepted | arrived | in_progress | completed | cancelled
quote_id bigint price locked at request time
version int for conditional updates
created_at timestamp
drivers
driver_id bigint primary key
current_trip_id bigint null when freeThe short TTL on driver keys matters: a driver whose phone loses signal drops out of search on their own, with no cleanup job.
High-level design
Split the system into a fast, lossy location path and a slower, strict trip path.
- Location gateways hold driver connections. Each update goes onto a queue partitioned by region, so a burst in one city doesn't slow another.
- The location service consumes the queue, computes the driver's cell, moves them between cell sets if they crossed a boundary, and overwrites their current position.
- The search service answers "nearby drivers" by reading the rider's cell and its neighbours from the in-memory index.
- The dispatch service takes a ride request, ranks candidate drivers by ETA, and offers the trip to one driver at a time. The trip service owns the trip state machine and the durable trip record.
Where it breaks
Write every location update to the main database, and the database falls over. Hundreds of thousands of writes a second, each to a row overwritten four seconds later, pay for durability nobody needs.
The fix is to treat the current position as disposable state in memory, partitioned by region, and to keep the database for things that must not be lost: trips and payments. The new risk is losing an in-memory node. That's acceptable - drivers resend within seconds, so the index rebuilds itself. What is not acceptable is losing a trip, which is why the trip store stays durable and separate.
Geospatial indexing: geohash, S2 and H3
A database index on latitude and longitude can't answer "within 2 km" efficiently. Instead, divide the map into cells, give each cell an ID, and index drivers by cell.
- Geohash interleaves latitude and longitude bits into a base-32 string. A shared prefix means a shared containing cell, so it works in any key-value store. Two nearby points can have very different hashes across a cell edge, so always search the neighbouring cells too.
- Google's S2 projects the sphere onto the six faces of a cube and orders cells along a Hilbert curve. Each cell has a 64-bit ID, and nearby cells tend to have nearby IDs, which makes range scans efficient.
- H3, which Uber open-sourced, tiles the earth with hexagons at 16 resolutions. Each hexagon's six neighbours are all the same distance from its centre, which makes "rings" around a rider and per-cell demand counts for surge pricing simple to compute.
- Pick a cell size where a typical cell holds tens of drivers. Search the rider's cell and its neighbours; if too few drivers are found, widen by one ring.
Matching and "one driver, one trip"
Two dispatchers might pick the same free driver for two different riders. Prevent this with a conditional write on the driver record: set current_trip_id only if it is still null. Exactly one of the two writes succeeds; the loser moves to the next candidate.
- Offer to one driver at a time with a short timeout. If they decline or the timer expires, release them and offer to the next.
- Move the trip through the state machine with versioned, conditional updates, so a late "accept" can't revive a cancelled trip.
- Keep dispatch for a region on one set of nodes, so contention stays local and no cross-region lock is needed.
Surge pricing and ETA
Surge is a per-cell ratio of open requests to free drivers over the last few minutes, smoothed so prices don't flicker. Compute it from the same location and request streams, a few seconds behind real time. ETA comes from a road-graph routing engine with live traffic, not straight-line distance - a driver 500 m away across a river may be the worst choice.
What interviewers look for
- Separating the write-heavy, lossy location path from the strict, durable trip path - with numbers to justify it.
- A concrete geospatial index: what a cell is, how neighbours are found, and how cell size is chosen.
- A clear guarantee that a driver can't be double-booked, using a conditional write rather than hope.
- Partitioning by region, and a sensible answer for what happens when a node holding live locations fails.
Frequently asked questions
How does Uber find nearby drivers quickly?
+
Divide the map into cells using a scheme such as geohash, S2 or H3, and keep an in-memory index of which drivers are in each cell. A search reads the rider's cell and its neighbours, then ranks the drivers found by estimated time to pickup.
Should driver locations be stored in a database?
+
Not the hot copy. The current position is overwritten every few seconds and rebuilt by the next update, so it belongs in memory with a short expiry. Keep history in cheap append-only storage if you need it.
What is the difference between geohash, S2 and H3?
+
Geohash encodes rectangular cells as base-32 strings and is easy to store anywhere. S2 maps the sphere onto a cube and numbers cells along a Hilbert curve with 64-bit IDs. H3 uses hexagons, whose neighbours are all equidistant, which makes radius searches and per-area aggregation simpler.
How do you stop two riders from getting the same driver?
+
Assign the driver with a conditional write that only succeeds if the driver has no current trip. Only one dispatcher's write can win; the other sees the failure and offers its rider the next candidate.
Why partition by region?
+
Rides are local. Partitioning location data, dispatch and trips by region keeps matching contention inside one partition, and a failure in one region doesn't take down the rest.