DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

Design Uber — Geospatial Matching, Live Location & Surge Pricing (with Production .NET Code)

"Design Uber" comes down to one question that sounds easy and isn't: given a rider standing here, find the nearby available drivers — right now, among millions of them, while every one of those drivers is moving and pinging a new location every few seconds. Do it the obvious way — compute the distance from the rider to every driver — and you're doing millions of calculations per request. The entire interview is the data structure that turns "search everywhere" into "search the few blocks around the rider."

This is the condensed walkthrough; the full guide (estimates, API, data model, and the full production .NET 9 code) is on my site 👇

Full guide: https://prepstack.co.in/blog/design-uber-system-design

The design at a glance

Concern Decision
Find nearby drivers Geospatial index (geohash / quadtree / H3) — search the rider's cell + neighbors
Driver locations In-memory geo index (Redis GEO), sharded by region — not a DB table
Location ingestion Persistent connections; update the index, don't write each ping to a DB
Matching Rank nearby drivers by ETA, offer, and lock the driver to avoid double-dispatch
Surge Per-cell demand ÷ supply ratio, updated continuously
Live trip Stream driver location to the rider over a WebSocket

Two forces: a write firehose and a proximity read

The write side is the monster:

Drivers: ~5M active, each pings location every ~4s
  -> 5,000,000 / 4  ~  1.25M location updates/sec

Ride requests: millions/day  ~  hundreds/sec, each a nearby-driver search
Enter fullscreen mode Exit fullscreen mode

You cannot write 1.25M rows/sec to a relational database, so driver location lives in an in-memory, sharded geo index updated in place; and the proximity search must be cell-bounded, never a scan over millions of drivers.

Drivers == location every ~4s ==> [ Location ingestion ] --> [ Geo index (Redis GEO), sharded by region ]
                                                                    ^
Rider request (pickup) --> [ Matching service ] -- search nearby cells
                                |  rank by ETA - offer - LOCK driver
                                v
                         [ Ride store ] - [ Trip tracking (WebSocket) ] - [ Surge (per cell) ]
Enter fullscreen mode Exit fullscreen mode

The hard parts

Geospatial indexing — the heart of it. The naive approach — distance from the rider to every driver — is O(N) per request and dies at millions of drivers. Instead, partition space into cells and bucket drivers by cell; a proximity search only examines the rider's cell and its neighbors.

  • Geohash: interleave lat/lng bits into a string. Nearby points share a prefix, so a cell is a prefix and "nearby" is "matching prefix + the 8 neighbor cells." Simple, and what Redis GEO uses under the hood.
  • Quadtree: recursively split space into four quadrants; dense areas (downtown) subdivide deeper — adaptive to density.
  • S2 (Google) / H3 (Uber): hierarchical global cell systems; Uber's H3 uses hexagons (uniform neighbor distance, no corner problems).
A geohash cell + its 8 neighbors — a "nearby" search scans these 9 cells:

      +-----+-----+-----+
      | NW  |  N  | NE  |
      +-----+-----+-----+
      |  W  |  *  |  E  |     * = the rider's cell
      +-----+-----+-----+
      | SW  |  S  | SE  |
      +-----+-----+-----+

You examine ~9 cells' worth of drivers, never all N drivers.
Enter fullscreen mode Exit fullscreen mode

The location write firehose. 1.25M updates/sec can't hit a database. Drivers stream location over persistent connections; the ingestion layer updates the driver's position in the in-memory geo index in place (one current position, not a history of rows). The index is sharded by region (geohash prefix), so each shard owns its slice of the map and the write load spreads. Hot regions get finer shards.

Matching — closest isn't the answer, best ETA is. A nearby search returns candidates; ranking by straight-line distance is wrong — a driver 200m away across a river is farther by road than one 500m away on the same street. Rank by ETA (road-network + traffic). Then offer the ride to the top driver and lock them (a short hold) so a second rider's search can't dispatch the same car; if they decline or time out, release and offer the next. Locking prevents double-dispatch.

Surge pricing. Per cell, compute demand ÷ supply — open requests vs available drivers. When demand outstrips supply, a surge multiplier rises for that cell, pricing the scarcity and nudging drivers toward it. A continuously recomputed, per-region number — and a feedback loop, so tune it or it oscillates.

Live trip tracking. Once matched, the driver's location streams to the rider in real time — the exact push-over-WebSocket problem from the chat design.

Scaling gotchas

  • Ingestion firehose: in-memory geo index, sharded by region; persistent connections, not per-ping HTTP.
  • Hot cells (downtown, a stadium at closing): split finer; the index adapts to density, or that cell melts.
  • Proximity search: cell-bounded and served from memory — the whole point.
  • Consistency: driver location is eventually consistent (seconds stale is fine); dispatch uses a lock where it matters.
  • Durability split: rides persist relationally; location stays ephemeral in Redis (a lost ping just re-arrives in 4s).
  • Privacy: streaming precise movement of millions is a serious responsibility — minimize retention.

I shipped this in production (Mattrx)

Mattrx isn't ride-hailing, but its geo-analytics run on exactly this proximity-search machinery. Conversions carry coordinates, and marketers ask location questions: "how many conversions within 5 km of this store?" (footfall attribution) and "render a heatmap of engagement." V1 answered those with a Haversine distance filter scanned across the CampaignEvents table (1.2B rows) — a full range scan per query, p95 ~1,800 ms that pinned the DB whenever a marketer dragged the map. We moved recent conversion events into per-tenant Redis GEO sets on ingestion, so a radius question becomes a geohash-bounded GEOSEARCH over a few cells:

Metric Before After
"Conversions within 5 km of a store" p95 ~1,800 ms (Haversine scan) ~12 ms (Redis GEOSEARCH)
Rows examined per query full CampaignEvents range scan a few geohash cells
Store-attribution DB load heavy per query offloaded to the Redis geo index
Geo-heatmap render seconds, laggy panning sub-second, smooth

GeoAddAsync upserts a conversion's position by encoding it into a geohash score, and GeoSearchAsync with a GeoSearchCircle does the cell-bounded radius query natively — so a "near this store" question touches a handful of geohash cells in memory instead of Haversine-scanning a billion rows in Azure SQL, the whole difference between an 1,800 ms map drag and a 12 ms one. (Full .NET 9 geo index is in the post.)

The model to carry forward

Ride-hailing is a proximity-search problem wrapped in a location firehose. You never search all drivers — you bucket the map into cells, drop drivers into cells, and a nearby search touches only the cells around the rider. Keep that index in memory because the write rate is enormous, shard it by region so it scales and adapts to density, match on ETA rather than distance, and lock a driver during an offer so you never book one car twice.

Three habits it teaches: reach for a spatial index immediately ("distance to every driver" fails; "cell + neighbors" scales); keep the firehose out of your database (a current-location-per-driver index in memory beats a million writes/sec to disk); match on ETA, lock on dispatch (distance is a lie the map tells; the driver lock keeps matching correct).

The full guide has the estimates, API, data model, all the hard parts in depth, scaling, the complete production .NET 9 Redis GEO code, and the "when it's overkill" section:

https://prepstack.co.in/blog/design-uber-system-design

Originally published on PrepStack.

Top comments (0)