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:
- Drivers stream location continuously while online.
- Rider requests a ride; the system finds nearby available drivers.
- Match and dispatch one driver, exactly one, to the rider.
- Track the trip in real time until it completes.
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:
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
| Quantity | Assumption | Result |
|---|---|---|
| Online drivers | ~5M concurrently | the population that pings |
| Location pings | every ~4s per driver | ~1.25M location writes/sec |
| Ride requests | far rarer than pings | tens of thousands/sec at peak |
| Proximity query | per request, drivers within ~radius | must return in ms |
| Location durability | only "current" matters | overwrite 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
| Endpoint | Does | Notes |
|---|---|---|
POST /v1/drivers/location | driver reports location | the firehose; tiny payload, fire-and-forget, ~every 4s |
POST /v1/rides | rider requests a ride | idempotency key; returns the matched driver + trip id |
WS /v1/trips/:id | real-time trip updates | persistent 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.
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… | Mode | What the user sees |
|---|---|---|
| A geo-index shard | degrade | a few seconds of stale locations in that region; next pings refill; replica takes over |
| Location ingest lag | fail open | match on slightly older positions; widen the radius if sparse |
| Driver claim / dispatch | fail closed | atomic claim; never offer one driver to two riders |
| Trip write | fail closed | do 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
- SLI / SLO: match latency (request to driver assigned) under target, say 99% within a few seconds; and the match success rate (requests that find a driver).
- Location freshness: ingest lag, the age of the positions the matcher is using. A naive design never measures this and matches on stale data.
- Per-region health: hotspots, shard load, supply/demand imbalance by city. Regional, because the system is sharded regionally.
- The three pillars: metrics for match latency and ingest lag, traces across ingest and matching, structured logs keyed by trip id.
- Alert on pain and burn: page on match-latency budget burn and shard saturation, not raw CPU.
Maps to S10 (observability). See the observability cheat sheet.
Step 8
Trade-offs along the six core axes
| Axis (S1) | The call here | Why it is least-wrong |
|---|---|---|
| Consistency vs availability | split by data type | location eventual (stale is fine); trip + claim strongly consistent |
| Latency vs cost | in-memory geo index | pay RAM and overwrite churn to answer proximity in ms |
| Durability vs throughput | do not persist the firehose | current location is disposable; the next ping refills a loss |
| Space vs time | cell index over coordinate scan | store membership per cell to turn a scan into 9 small reads |
| Coupling vs cohesion | shard by geography | a city is self-contained; one region cannot sink another |
| Simplicity vs capability | atomic claim, not a saga | a 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)
################################################################################
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)
| Module | Does | Store | The one call |
|---|---|---|---|
| Fare quote | price + ETA shown before you request | reads surge + ETA models | quote is locked to an id; the price can't jump between quote and request |
| Trip request | the ride booking itself | Trip store (ACID) | idempotent (Step 3); one tap ≠ two cars |
| Live tracking | driver dot + ETA on the map | reads geo index over WS | push, don't poll; same channel as messaging |
| Trip history / receipts | past trips, invoices, tax | warehouse + object store (PDF) | read-model off the event bus, not the hot path |
B · Supply side (driver-facing)
| Module | Does | Store | The one call |
|---|---|---|---|
| Location firehose | ~1.25M pings/sec (Step 6B) | in-memory geo index | overwrite, never persist; loss self-heals in 4s |
| Dispatch / offers | offer a matched ride, accept/decline | driver-status (atomic) | atomic claim (Step 6C); no double-dispatch |
| Driver onboarding | signup → docs → vehicle → bg check → active | relational + workflow state | a long-running async saga (days), not a request |
| Earnings & incentives | per-trip pay, quests, instant cashout | ledger + payout wallet | derived from trip + fare events; strongly consistent money |
C · Marketplace: price · ETA · money · trust
| Module | Does | Store | The one call |
|---|---|---|---|
| Surge / dynamic pricing | multiplier per cell to clear supply vs demand | streaming aggregates per H3 cell + model serving | computed on a stream over the same geo cells; eventually consistent, locked into a quote |
| ETA & routing | driver→rider ETA, trip duration, route | road graph (partitioned) + live traffic tiles + ML | ETA is a prediction, not a computation; the fleet's own GPS is the traffic sensor |
| Supply positioning | heatmaps nudging drivers to demand | demand forecast + geo | read-only nudge; drivers aren't commanded |
| Fare calc + ledger | base+time+distance+surge−promo; the money truth | double-entry ledger (ACID) | immutable, auditable ledger is the source of truth |
| Payments | charge the rider via a PSP | tokenized cards, PSP | auth/hold at request, settle post-trip; retries idempotent (see the Stripe design) |
| Payouts | split fare, pay the driver, take rate, tax docs | payout wallet + ledger | async settlement off trip-completed events |
| Ratings & feedback | two-sided stars, drives deactivation | aggregate store | eventually consistent aggregate off the bus |
| Trust & safety | identity, fraud, SOS, incident, anomaly | risk store + ML serving | score async; fail closed on money, escalate on safety, don't block the ride |
D · Platform & data backbone (used by everything above)
| Module | Does | The one call |
|---|---|---|
| Event bus (Kafka) | every location, trip-state and payment change is an event | producers don't know their consumers; add analytics/fraud/surge without touching the ride path |
| Maps / geo platform | H3 cells, geocoding, the road graph | shared primitive under matching, ETA, surge and positioning alike |
| Data lake + stream/batch | events → warehouse for analytics + ML training | trips land cold for training; never on the hot path (Step 6B) |
| ML platform | feature store + model serving for surge/ETA/fraud | the same models serve three subsystems; train offline, serve online |
| Notifications | push / SMS / email fan-out | one fan-out service, every subsystem is a producer |
| Config & experiments | feature flags, A/B, staged rollout by city | roll 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.
| Discipline | Subsystems | Posture |
|---|---|---|
| Eventual · fail open | location firehose, geo index, surge multiplier, ETA, ratings, heatmaps | stale is fine; never block the ride; loss self-heals |
| Strong · fail closed | trip state, driver claim, fare ledger, payments, payouts, identity | exact and durable; refuse rather than corrupt money or double-dispatch |
| Async · off the bus | fraud scoring, analytics, ML training, notifications, receipts | runs 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
- Slot: Week 6 worked design. Pairs with WhatsApp (both are realtime) but the lever is geospatial indexing, not connections.
- Run it live: walk steps 1 to 5, then ask "how do you find drivers within 2km without scanning millions of rows?" and let the room invent cells before revealing geohash/S2/H3.
- The core sentence: "discretise the map into cells so proximity is a handful of small in-memory reads, and claim the driver atomically so no one is dispatched twice."
- The scale-up (Step 9): once the core lands, zoom out to the whole platform and land the staff-level line: "rides are eventually consistent and fail open; money and identity are strongly consistent and fail closed; everything else hangs off the event bus."