hub.gazar.dev · senior-to-staff · worked system design

Design Uber: ride-sharing

A fully worked answer in the course order: requirements, numbers, API, data, the high-level diagram, the deep dives that actually decide it, then reliability, observability, and the trade-offs. The lever here is geospatial: how do you take a million drivers each reporting their location every few seconds, and answer "who is near this rider, right now?" in milliseconds. Back to the course.

The prompt, scoped

"Design Uber" is "match a rider to a nearby driver in real time." We keep the spine: drivers stream their location, a rider requests a ride, the system finds nearby available drivers, dispatches one, and tracks the trip. Out of scope, said up front: surge pricing, payments (that is the Stripe design), turn-by-turn routing, and ratings. The hard part is the location firehose and the proximity query, not the rest.

Step 1

Requirements: functional and non-functional

The functional core:

Out of scope for the core answer (say it): pricing/surge, payments, routing/ETA internals, ratings. That is the right call for a 45-minute interview. But a real ride touches all of them, so in Step 9 we zoom out and design the whole ride-sharing platform those pieces belong to.

The non-functional shape:

~1M location writes/sec match in seconds geo query in ms location eventually-consistent no double-dispatch

Two facts drive everything: an enormous stream of ephemeral location updates, and a latency-critical "who is near me" query on top of it. A slightly stale driver dot is fine; dispatching the same driver to two riders is not.

Maps to S2 (requirements) and S1 (consistency vs availability, split by data type).

Step 2

Estimation: the numbers that bound the design

QuantityAssumptionResult
Online drivers~5M concurrentlythe population that pings
Location pingsevery ~4s per driver~1.25M location writes/sec
Ride requestsfar rarer than pingstens of thousands/sec at peak
Proximity queryper request, drivers within ~radiusmust return in ms
Location durabilityonly "current" mattersoverwrite in memory; do not persist every ping

The number that decides the architecture

~1.25M location writes per second, almost all immediately overwritten, feeding a proximity query that must answer in milliseconds. That rules out a durable write per ping and a scan-the-table query. It demands an in-memory, spatially-partitioned index where a write is an overwrite and a read touches only nearby cells.

Maps to S2 (capacity estimation).

Step 3

API: the few operations

EndpointDoesNotes
POST /v1/drivers/locationdriver reports locationthe firehose; tiny payload, fire-and-forget, ~every 4s
POST /v1/ridesrider requests a rideidempotency key; returns the matched driver + trip id
WS /v1/trips/:idreal-time trip updatespersistent connection; driver location pushed to the rider

The location report is the hot, lossy write; the ride request is the rare, important one (idempotent, so a retry does not request two cars). Live trip tracking rides a persistent connection, like the messaging design.

Maps to S5 (API design): idempotent request, a firehose write, a persistent channel. See the API cheat sheet.

Step 4

Data model and store choices

Driver(id PK, status=online|on_trip|offline, current_cell)
DriverLocation[driver_id] -> {lat, lng, ts}        current only, overwritten
GeoIndex[cell_id] -> set of driver_ids             the spatial index
Trip(id PK, rider_id, driver_id, state, started_at)
Rider(id PK, ...)

stores
  Location + geo index .. 1M writes/sec, ephemeral, read by proximity   -> in-memory (Redis), sharded by geo cell
  Trip ................... must be correct + durable, state machine      -> relational / strongly-consistent
  Driver / rider profiles  ordinary lookups                              -> relational

Why these stores

  • Location and the geo index are a write firehose of disposable data, read by "who is in these cells": an in-memory store (Redis with geo, or a custom cell index) sharded by region. A ping overwrites; nothing is persisted per update.
  • Trips are the opposite: a durable, correct state machine (requested → matched → on-trip → done), so a relational store with transactions.

Maps to S3 (store from access patterns) and S4 (shard the index by geography). See the data-stores cheat sheet.

Step 5

High-level architecture

Two flows over one index. The location ingest service takes the driver firehose and overwrites each driver's cell in the in-memory geo index. The matching service takes a ride request, queries the geo index for nearby drivers, picks one, writes a trip, and dispatches the offer to that driver. Separating the lossy write path from the careful match path is the whole shape.

Drivers ~5M online Location ingest firehose Geo index Redis · S2 cells · sharded Matching service query · pick · dispatch Rider requests Trip store ACID state machine loc pings · 4s overwrite cell request ride nearby drivers? create trip dispatch offer
The driver firehose (top) overwrites cells in the in-memory geo index. A ride request (bottom) drives the matching service to query the index for nearby drivers, write a trip to the ACID store, and dispatch the offer back to a driver (green). Orange = the in-memory index, blue = the durable trip store.

Maps to S7 (architectural styles) and S4 (geo-sharded index).

Step 6

Deep dive A: geospatial indexing

The core technique. "Find drivers within 2km" is impossibly slow as a scan over millions of rows, so you carve the world into cells and index drivers by cell. A proximity query then touches only the rider's cell and its neighbors.

approaches (all the same idea: discretise the map)
  geohash   .. encode lat/lng to a string; shared prefix = nearby
  S2 / H3   .. hierarchical cells over a sphere (Google S2, Uber H3)
  quadtree  .. recursively split dense areas into smaller cells

query "drivers within radius of rider"
  1. compute the rider's cell
  2. gather that cell + its 8 neighbors (covers the radius)
  3. read the driver sets from those cells (small, in-memory)
  4. fine-filter by exact distance, sort by ETA

write "driver moved"
  cell changed?  remove from old cell set, add to new cell set
  same cell?     just update the stored point

Why cells beat a coordinate scan

A query bounded to ~9 small cell-sets is O(drivers nearby), not O(all drivers). Cell size is the tuning knob: too big and each cell holds too many drivers; too small and a radius spans too many cells. Uber's H3 (hexagons) and Google's S2 are the productionised versions of this one idea.

Maps to S4 (partitioning by a key, here a spatial one) and S1 (space vs time).

Step 6 · continued

Deep dive B: the location firehose

A million-plus writes per second of data that is obsolete in seconds. The trick is to treat it as disposable and keep it in memory, sharded so no single node sees the whole planet.

ingest a ping
  - tiny payload {driver_id, lat, lng, ts}, fire-and-forget (UDP-ish)
  - route to the shard that owns the driver's region
  - OVERWRITE current location; do not append, do not persist to disk
  - if the cell changed, update the GeoIndex set membership

sharding
  - partition the in-memory index by geography (region -> shard)
  - a city's load stays on its shard; the planet never hits one node
  - replicate each shard for failover; losing one loses a few seconds
    of "current location" that the next ping refills

The senior point

Do not durably persist the firehose. Current location is the only thing that matters, the next ping is 4 seconds away, and a lost update self-heals. Persisting every ping would be a massive, pointless write load. If you need trip history, sample it separately into a cold time-series store, off the hot path.

Maps to S4 (sharding the write path) and S9 (a lost ping self-heals; design for it).

Step 6 · continued

Deep dive C: matching without double-dispatch

Selecting a driver is easy; making sure one driver is not offered to two riders at once is the real work. This is a concurrency problem, not a geo one.

match
  1. candidates = geo query (nearby + status=online)
  2. rank by ETA / fairness / acceptance likelihood
  3. OFFER to the best driver, with a short timeout

the race: two riders, one nearby driver
  - claim the driver atomically before offering:
      SET driver:status online -> offering   (compare-and-set)
  - only the request that wins the CAS may offer that driver
  - on accept  -> on_trip;  on decline/timeout -> back to online,
    offer the next candidate
  => a driver can be in exactly one active offer at a time

The trap interviewers set

"Two requests pick the same driver." If you only read-then-offer, both can offer the same car. The fix is an atomic claim (a compare-and-set on driver status, or a short lock) so exactly one request owns the driver while the offer is outstanding. This is the same fail-closed-on-the-claim move as seats in a ticketing system.

Maps to S6 (idempotency and atomic claims) and S9 (fail closed on the dispatch).

Step 7

Reliability & failure design

If this fails…ModeWhat the user sees
A geo-index sharddegradea few seconds of stale locations in that region; next pings refill; replica takes over
Location ingest lagfail openmatch on slightly older positions; widen the radius if sparse
Driver claim / dispatchfail closedatomic claim; never offer one driver to two riders
Trip writefail closeddo not start a trip you cannot durably record

The location path is deliberately the lossy, fail-open side; the match and trip path is the careful, fail-closed side. Regions are bulkheaded: each city is its own shard, so a surge or outage in one does not drain capacity from the rest. Calls are bounded with timeouts and breakers, and the driver offer has a short timeout so a non-responsive driver releases the claim quickly.

Maps to S9 (designing for failure): fail open on location, closed on dispatch. See the resilience cheat sheet.

Step 7 · continued

Observability & operability

Maps to S10 (observability). See the observability cheat sheet.

Step 8

Trade-offs along the six core axes

Axis (S1)The call hereWhy it is least-wrong
Consistency vs availabilitysplit by data typelocation eventual (stale is fine); trip + claim strongly consistent
Latency vs costin-memory geo indexpay RAM and overwrite churn to answer proximity in ms
Durability vs throughputdo not persist the firehosecurrent location is disposable; the next ping refills a loss
Space vs timecell index over coordinate scanstore membership per cell to turn a scan into 9 small reads
Coupling vs cohesionshard by geographya city is self-contained; one region cannot sink another
Simplicity vs capabilityatomic claim, not a sagaa compare-and-set on driver status is enough to stop double-dispatch

The whole design is one question answered fast and one answered safely: where are the nearby drivers (in-memory cells, eventually consistent), and is this driver claimable right now (atomic, fail closed). Say that and the boxes follow.

Maps to S1 (the six core trade-offs) and S12 (present and defend).

Step 9

Beyond the core: the whole ride-sharing platform

Steps 1–8 answered the interview: match a rider to a nearby driver. But a real Uber ride is wrapped in a dozen more subsystems. Before you request you want to see a price and an ETA; you have to pay at the end and the driver has to get paid; both sides had to be onboarded and trusted; and every event has to feed analytics, ML and safety. Same method, applied per subsystem. Here is the full platform, grouped by the four jobs it does: serve demand, manage supply, run the marketplace (price/ETA/money/trust), and the platform backbone underneath.

                         RIDER APP                         DRIVER APP
                             |                                  |
                 +-----------+----------------- EDGE -----------+-----------+
                 |     API gateway  ·  auth / identity  ·  rate-limit  ·  TLS     |
                 +-----------+----------------+---------------+-----------+
                             |                |               |
   === DEMAND SIDE ======== | ==== CORE ===== | ============= | == SUPPLY SIDE ===
   |  Trip request          |   Matching &    |   Dispatch    |  Driver location   |
   |  Fare quote  <--------+ |   dispatch      |   offers &    |    firehose  (core)|
   |  ETA / route preview   |   (Steps 1-8)   |   trip WS     |  Driver onboarding |
   |  Live trip tracking(WS)|                 |               |  Earnings / quests |
   |________________________|_________________|_______________|____________________|
                             \                |                /
   === MARKETPLACE INTELLIGENCE ===  === MONEY ===        === TRUST & SAFETY ===
   |  Surge / dynamic pricing   |  Fare calc + ledger  |  Identity & bg checks   |
   |  ETA & road-graph models   |  Rider payments(PSP) |  Fraud / risk scoring   |
   |  Supply positioning        |  Driver payout wallet|  Two-sided ratings      |
   |  Experiment platform       |  Receipts / invoices |  SOS / incident / support|
   |____________________________|______________________|__________________________|
                             |                |                |
   ############################ PLATFORM & DATA BACKBONE ###########################
     Event bus (Kafka)   ·   Maps/geo (H3, geocoding, road graph)   ·   Notifications
     Data lake + stream/batch   ·   ML feature store & serving   ·   Config / flags
     Service mesh   ·   Observability (metrics · traces · logs)
   ################################################################################
The core match/dispatch loop (Steps 1–8) sits in the middle. Demand-side and supply-side services sit either side; the marketplace layer (price, ETA, money, trust) wraps the ride; and everything is stitched together by the event bus and data/ML backbone underneath.

Maps to S7 (decompose a platform into services) and S8 (an event backbone decouples the producers from the dozen consumers).

Step 9 · module inventory

Every subsystem, one line each

Each module gets the same treatment as the core: what it does, where its data lives, and the one design call that defines it.

A · Demand side (rider-facing)

ModuleDoesStoreThe one call
Fare quoteprice + ETA shown before you requestreads surge + ETA modelsquote is locked to an id; the price can't jump between quote and request
Trip requestthe ride booking itselfTrip store (ACID)idempotent (Step 3); one tap ≠ two cars
Live trackingdriver dot + ETA on the mapreads geo index over WSpush, don't poll; same channel as messaging
Trip history / receiptspast trips, invoices, taxwarehouse + object store (PDF)read-model off the event bus, not the hot path

B · Supply side (driver-facing)

ModuleDoesStoreThe one call
Location firehose~1.25M pings/sec (Step 6B)in-memory geo indexoverwrite, never persist; loss self-heals in 4s
Dispatch / offersoffer a matched ride, accept/declinedriver-status (atomic)atomic claim (Step 6C); no double-dispatch
Driver onboardingsignup → docs → vehicle → bg check → activerelational + workflow statea long-running async saga (days), not a request
Earnings & incentivesper-trip pay, quests, instant cashoutledger + payout walletderived from trip + fare events; strongly consistent money

C · Marketplace: price · ETA · money · trust

ModuleDoesStoreThe one call
Surge / dynamic pricingmultiplier per cell to clear supply vs demandstreaming aggregates per H3 cell + model servingcomputed on a stream over the same geo cells; eventually consistent, locked into a quote
ETA & routingdriver→rider ETA, trip duration, routeroad graph (partitioned) + live traffic tiles + MLETA is a prediction, not a computation; the fleet's own GPS is the traffic sensor
Supply positioningheatmaps nudging drivers to demanddemand forecast + georead-only nudge; drivers aren't commanded
Fare calc + ledgerbase+time+distance+surge−promo; the money truthdouble-entry ledger (ACID)immutable, auditable ledger is the source of truth
Paymentscharge the rider via a PSPtokenized cards, PSPauth/hold at request, settle post-trip; retries idempotent (see the Stripe design)
Payoutssplit fare, pay the driver, take rate, tax docspayout wallet + ledgerasync settlement off trip-completed events
Ratings & feedbacktwo-sided stars, drives deactivationaggregate storeeventually consistent aggregate off the bus
Trust & safetyidentity, fraud, SOS, incident, anomalyrisk store + ML servingscore async; fail closed on money, escalate on safety, don't block the ride

D · Platform & data backbone (used by everything above)

ModuleDoesThe one call
Event bus (Kafka)every location, trip-state and payment change is an eventproducers don't know their consumers; add analytics/fraud/surge without touching the ride path
Maps / geo platformH3 cells, geocoding, the road graphshared primitive under matching, ETA, surge and positioning alike
Data lake + stream/batchevents → warehouse for analytics + ML trainingtrips land cold for training; never on the hot path (Step 6B)
ML platformfeature store + model serving for surge/ETA/fraudthe same models serve three subsystems; train offline, serve online
Notificationspush / SMS / email fan-outone fan-out service, every subsystem is a producer
Config & experimentsfeature flags, A/B, staged rollout by cityroll out per city because the system is already sharded per city

Maps to S3 (a store per access pattern), S4 (shard each subsystem on its own key) and S8 (event-driven decoupling).

Step 9 · deep dive

The four that reward depth

Surge pricing rides on the same geo cells

Surge is a multiplier per cell per short window. A stream job aggregates open requests vs available drivers per H3 cell (the very cells the matcher uses), blends a demand-forecast model, and publishes a multiplier every few seconds. It is eventually consistent (a slightly stale multiplier is fine) but locked into the fare quote so the price a rider accepted can't move underneath them. The geospatial index you built for matching is reused wholesale.

ETA is a prediction, and the fleet is the sensor

Routing gives you a path over a precomputed road graph (contraction hierarchies for millisecond shortest-path). But the ETA on top is an ML prediction over live traffic + historical speeds. The clever part: the live-speed signal comes from the same driver location firehose. Drivers moving through the map are the traffic sensors, so the ingest path you built for matching doubles as the input to ETA.

Money is a ledger, and it is strongly consistent

Fares, rider charges, the platform take rate and driver payouts all live in a double-entry ledger, the auditable source of truth. Payment is async and post-trip: authorise/hold at request time, capture on completion, settle to the driver's wallet off the trip.completed event, with idempotent retries so a redelivered event never double-pays. This is the fail-closed half of the system, the opposite discipline to the fail-open location firehose.

Trust & safety scores off the bus, never in the ride path

Identity checks (docs, background, live face match) gate onboarding. Fraud and risk (GPS spoofing, collusion trips, payment fraud, incentive gaming) score asynchronously off the event bus so they never add latency to a ride. The rule of thumb: fail closed on money (block a suspicious payout), escalate hard on safety (SOS, route-deviation anomaly), but don't block the ride experience for a risk signal that can be reviewed after the fact.

Maps to S1 (consistency split by data type, again) and S11 (ML/data as first-class subsystems).

Step 9 · the through-line

One consistency map for the whole platform

Zoom all the way out and the entire platform sorts onto the same axis you drew for the core: what can be stale and lossy, versus what must be exact and durable.

DisciplineSubsystemsPosture
Eventual · fail openlocation firehose, geo index, surge multiplier, ETA, ratings, heatmapsstale is fine; never block the ride; loss self-heals
Strong · fail closedtrip state, driver claim, fare ledger, payments, payouts, identityexact and durable; refuse rather than corrupt money or double-dispatch
Async · off the busfraud scoring, analytics, ML training, notifications, receiptsruns behind the ride via Kafka; add or remove without touching the hot path

That is the staff-level move: not a bigger box diagram, but one organising principle that tells you, for any new feature, which side of the line it belongs on. Rides are eventually consistent and fail open; money and identity are strongly consistent and fail closed; everything else hangs off the event bus.

Maps to S1 (the six axes) and S12 (present the platform as one idea, not fifteen boxes).

Use this in the cohort