Index/Uber / Delivery Tracking

SponsorGitHub
Design15 min

Uber / Delivery Tracking

Five million drivers publishing position every four seconds, matched to riders in real time.

Primary concepts and the hard part

Concepts
geospatial indexing (geohash/S2/quadtree)high-frequency location writesin-memory statematching under contentionWebSockets for live trackingstream processingdistributed lockingsagas
The hard part they’re probing
Two distinct problems glued together. (1) Millions of drivers writing location every 4 seconds destroys any disk-backed index. (2) Matching is a contention problem — two riders must not be assigned the same driver. Candidates who only solve the geospatial half miss the harder half.

Requirements

Functionalwhat it must do
Drivers publish location continuously
Rider requests a ride from A to B
System finds nearby available drivers and offers the trip
Driver accepts; rider sees live driver position until pickup and dropoff
Trip lifecycle: requested → matched → en route → in progress → completed → paid
Non-functionalhow well it must do it
Match within a few seconds
Location freshness ~5 seconds (stale-but-recent is fine)
Never double-assign a driver (strong consistency at the assignment point only)
High availability; a region outage must not take down other regions
Out of scopedeliberately left out
Pricing/surge algorithms
Routing/ETA computation internals (treat as a service)
Fraud
Scale
Drivers online:     5M
Location updates:   every 4s → 5M/4 = 1.25M writes/sec   ← the headline number
Ride requests:      10,000/sec
Location payload:   ~100B → 125 MB/sec sustained
Conclusion
1.25M writes/sec of ephemeral data that is worthless after 10 seconds. That single fact dictates an in-memory store for current position, with history written asynchronously to a separate cold path. Anyone proposing a disk-backed geospatial index here has missed the point.

API / Model

Driverpersistent WS or frequent POST
POST
/v1/drivers/location
{driver_id, lat, lng, heading, ts}
202Accepted
WS
driver stream
receives trip offers
101Switching Protocols
Rider
POST
/v1/rides
{pickup, dropoff}
201Createdride_id
GET
/v1/rides/{id}
200OKstatus
WS
rider stream
receives driver position updates
101Switching Protocols
primary or partition keysort keyforeign key → referenced columnHover a table or column to trace its keys

High-level architecture

DatabaseCacheObject storageQueue / streamFocusClick a node for details

Two workloads meet in this design: a constant stream of driver positions through the Location Gateway, and rider requests that go through matching and become trips. The Redis geo index is where they connect.

  1. A rider's POST /rides creates a trip in the Ride Service with state REQUESTED and hands it to the Matching Service, which runs GEOSEARCH on the Redis geo index within 3 km, covering the target cell and its 8 neighbours.
  2. It filters candidates by availability, vehicle type, rating and heartbeat.
  3. It ranks the rest by routed ETA from the Routing / ETA Service rather than by straight-line distance.
  4. It acquires a per-driver lock with SET NX PX and a 30-second expiry, plus a fencing token, so the driver can't be assigned twice.
  5. It offers the trip and waits 15 seconds. A reject or timeout moves it to the next candidate.
  6. Once a driver accepts, the Trip Service runs the state machine from MATCHED through ARRIVING and IN_PROGRESS to COMPLETED, records each change in trips and trip_events, and starts the saga on completion: charge, pay the driver, send the receipt.

The geo index is fed by the location paths. Every 4 seconds a driver's position reaches the Location Gateway, which overwrites that driver's entry synchronously, with a 30-second TTL that doubles as liveness, and sends the ping asynchronously to Kafka location.stream. Flink / Spark streaming turns that stream into ETA models and analytics and writes history to location_hist in S3. For drivers on an active trip only, the gateway also publishes to trip:{id}, throttled to one update every 2 seconds, which the rider's live map subscribes to.


Trade-offs and deep dives

01
Why geohash, and the boundary trap

Geohash encodes lat/lng into a string where nearby points share a prefix, turning a 2D proximity query into a 1D prefix scan any index can serve. The trap: two points can be 10 metres apart but sit either side of a cell boundary and share no prefix. Always query the target cell plus its 8 neighbours, then filter by true distance. Forgetting this produces a system that mysteriously can't find the driver parked across the street.

02
Geohash vs quadtree vs S2
  • Geohash: dead simple, works with any string-prefix index, fixed grid so dense cities and empty ocean get the same treatment.
  • Quadtree: subdivides only where density is high, adapting to uneven distribution. Better for wildly varying density, more complex to maintain under constant updates.
  • S2: projects the sphere onto a cube with a Hilbert curve; better locality, handles poles and the antimeridian correctly. What Uber and Google actually use.

For an interview, geohash with an explicit mention of S2 and the density caveat is a complete answer.

03
Why Redis and not a database

These writes are overwrites of ephemeral state with a useful lifetime of seconds. Durability is not required — if Redis loses a position, the driver publishes a new one four seconds later. Writing 1.25M/sec to a disk-backed store to hold data you'll throw away is the wrong trade. Persist to Kafka asynchronously for analytics, and keep the two paths separate.

04
Cells are the natural shard key

Partition Redis by city or region. All matching queries are local to a region, so no cross-shard scatter-gather. This also gives you regional fault isolation for free: San Francisco going down doesn't touch London. Say this explicitly — it's a strong availability argument.

05
The matching contention problem

This is the part most candidates miss. Two rider requests in the same neighbourhood will surface overlapping candidate lists. Without a mutex, both get offered the same driver. Use a short-TTL distributed lock (SET lock:driver:{id} {token} NX PX 30000) around the offer window, release it on accept-elsewhere, reject, or timeout. The TTL is the safety net so a crashed matcher doesn't strand a driver forever.

Be ready for the honest caveat: Redis locks are not a correctness guarantee under pause-and-resume scenarios (a matcher can be GC-paused past its TTL while believing it holds the lock). The fix is a fencing token — a monotonically increasing number issued with the lock that the trip service checks, rejecting any assignment carrying a stale token. Alternatively, make the final assignment a conditional write on the trip row (UPDATE ... WHERE driver_id IS NULL), so the database is the arbiter of record. That conditional write is the simplest correct answer and worth offering.

06
Consistency, scoped narrowly

Almost everything here is AP — driver positions, ETAs, nearby-driver lists can all be a few seconds stale with no harm. Exactly one operation needs strong consistency: the driver assignment. Isolating strong consistency to a single narrow operation, instead of applying it system-wide, is the mature design instinct.

07
Straight-line distance is not ETA

A driver 500m away across a river is 15 minutes away. Rank candidates by routed ETA from a routing service, not by geohash distance. Use geo distance only to generate the candidate set cheaply, then rank properly on a small set. That two-stage pattern (cheap retrieval, expensive ranking) is the same shape as search.

08
Live tracking must be filtered

Publishing all 5M drivers' positions to a pub/sub layer would be catastrophic. Only drivers on an active trip publish to a trip:{id} channel, and only the one rider subscribes. Throttle to ~1 update per 2 seconds; the map interpolates between points for smoothness. This turns an impossible fan-out into a fan-out of 1.

09
Trip completion is a saga

Charging the rider, paying the driver, and issuing a receipt span multiple services. Use a saga with compensating actions (refund on payout failure) rather than a distributed transaction, and make each step idempotent.

10
Driver liveness

A TTL on the Redis entry means a driver whose app crashed silently disappears from the index within 30 seconds. No cleanup job, no stale offers to phones that aren't listening. TTL-as-liveness is an elegant detail worth calling out.


Possible follow-up questions

6 questions·try answering before you reveal