Drivers move around a city, each phone reporting where they are. A rider opens an app, wants to see the cars near them, and gets matched to exactly one driver for a trip. Build the system that takes every driver's location, keeps it fresh, answers "who's near this rider" fast, and matches a rider to one driver — at the scale of a whole city's fleet.
Predict ~1.5 h · Read ~50 min
The problem i
A ride-sharing service sits between a fleet of moving drivers and a crowd of waiting riders. Every driver's phone reports a GPS position; every rider wants to see the cars near them and, when they ask, be handed one driver for a trip. On one machine, for a few drivers, this is almost nothing: keep a table of driver positions, update a row when a ping comes in, and scan the table when a rider asks who is nearby. Everything hard about this system shows up when the fleet gets large, and it turns on two hard facts.
Here is the first hard fact. The driver location updates are a firehose. A driver's app does not report once; it reports its position again and again, roughly every four seconds, the whole time the driver is online. That is Uber's own stated cadence, and Uber derived a design goal straight from it: a million location writes per second across the fleet. Sit with what that means. A million times a second, a driver's position changes, and four seconds later it is stale and changes again. If you treat each of those as a durable database write — a real write to disk that has to survive a crash — you are doing a million disk writes a second for data that is worthless in four seconds. The write rate is not a nuisance you tune away. It is the force that decides where driver location is even allowed to live.
Here is the second hard fact. A rider's question — "which cars are near me?" — cannot be answered by looking at every driver. When there are a handful of drivers, you scan them all and keep the close ones. When there are a million live positions, scanning all of them for every rider who opens the app does not scale; the read path buckles under the fleet size. So "near me" has to become a query that only ever looks at a small patch of the map, not the whole city. Turning that read into a bounded, local lookup — one that never scans the whole fleet — is the second force that shapes this design.
Put the two facts together and you get the shape of the whole problem. A colossal write rate on data that is stale in seconds, and a read that must never scan the fleet. Those two forces, not the matching step everyone thinks of first, are what this design is really about. Matching a rider to a driver — once you already have the nearby ones in hand — is the easy part.
At the scale we design for, one busy region has about a million drivers online at once, each pinging every four seconds. That is a planning figure, chosen because it is large enough to break the obvious first design, and it works out to about 250k location writes a second in that one region — which, summed across a service's regions, lands in the same neighborhood as Uber's stated 1M-a-second goal. Two timings are worth pinning now, because decisions later turn on them. A durable write to a real database takes about 10 milliseconds. Updating a value held in memory takes microseconds — thousands of times less. That gap is the whole reason the firehose cannot go to disk.
Functional requirements what it must do
Ingest every driver's location, continuously, at fleet scale.
Show a rider the drivers near them, fresh to within a few seconds.
Match a ride request to one nearby driver, and never double-book that driver.
Keep a durable record of each trip and its state.
Non-functional requirements what it must be
The location path keeps up at fleet scale with no single node as the bottleneck.
Losing one node loses no trips and only briefly loses the positions it held.
Under a demand flood, the marketplace degrades gracefully instead of collapsing.
Out of scope left out on purpose
The matching algorithm — which of the nearby drivers is the best one to offer
Turn-by-turn routing and ETA computation
Surge pricing and demand forecasting
The driver's location-stream transport
Payment, fare settlement, and rider-driver messaging
How the design works
The finished design, explained one piece at a time: first the contract the system serves, then the smallest version that works, then the version that survives scale — walked use case by use case, one deep dive at a time, with the finished diagram at the end.
The API
The whole product is these endpoints. Every box in the design below exists to serve one of them.
POST /location
The driver's app streams a location ping — fire-and-forget. The app reports its position and does not wait for anything back, because there is nothing to wait for. This call happens every four seconds, per driver, and it is the firehose. The response is an empty accepted acknowledgement, no body.contract ▾
# The driver's app streams a location ping — fire-and-forget.→POST /location {
driverId: "d_5521",
lat: 40.7128,
lng: -74.0060,
ts: 1735689600 }
←202 Accepted # no body — the app does not wait; there is nothing to wait for
GET /nearby
The rider's app asks who is nearby to draw the map. Answered with a short list of nearby drivers and their positions. This is the read that has to stay bounded to a small patch of the map, never a scan of the whole fleet.contract ▾
# The rider's app asks who is nearby — the map display.→GET /nearby?lat=40.7128&lng=-74.0060&radius=2000←200 OK {
drivers: [
{ driverId: "d_5521", lat: 40.7130, lng: -74.0058 },
{ driverId: "d_6604", lat: 40.7119, lng: -74.0071 }
]
}
POST /rides
The rider's app requests a ride. Answered with the created trip and its assigned driver. This is the one call that creates durable state — a trip the business keeps.contract ▾
# The rider's app requests a ride — the one call that creates durable state.→POST /rides {
riderId: "r_1029",
lat: 40.7128,
lng: -74.0060 }
←201 Created {
tripId: "t_88af13",
driverId: "d_5521",
status: "accepted"# requested → accepted → started → completed }
part 1 The single-server version
Start with the smallest thing that works: one server, one database, and a drivers table with a row per driver holding their latest position.
First, the vocabulary. A driver carries a phone that reports a location — a latitude and longitude — every few seconds. A rider opens the app, sees nearby drivers, and requests a trip, which is a single ride from request to completion. Those three words — rider, driver, trip — carry the whole product.
The day-one design is direct. When a driver's ping arrives, run an UPDATE to overwrite their row with the new position. When a rider asks who is nearby, run a SELECT over the drivers table, filter to a bounding box around the rider, and return the close ones. When a rider requests a ride, pick one of those nearby drivers, write a trips row, and hand the driver back. For a few dozen drivers in one town, this is genuinely fine, and nothing clever is missing.
The whole system on day one.
Following a request
A driver's position lands
a driver pings; the Location service runs UPDATE drivers SET lat, lng WHERE id — a durable write to disk, for a value stale again in about four seconds~10 ms
A rider sees who's nearby
a rider asks who's nearby; the Location service runs a full-scan SELECT over the drivers table, filtered to a bounding box — every row examined~10 ms
One hop dominates every path: the client's request crossing the public internet to reach the datacenter. That hop is tens of milliseconds — call it about 30 — and it is set by physical distance, not by anything we build. Everything after it happens inside a single datacenter, where the network between two machines is well under a millisecond — so those numbers are the actual work, not the wait: a durable write to disk about 10 ms, an indexed read about 5 ms. A hop between two datacenters would cost 10–100× the in-datacenter number, which is why the whole path stays in one region.
It just does not survive success, and it fails in three separate ways — each of which forces a piece of the real design. The first failure is the write rate. That UPDATE per ping is a durable write to disk, about ten milliseconds of real work to make the new position survive a crash. At a million drivers pinging every four seconds, that is hundreds of thousands of disk writes a second — and every one of them is durably saving a value that is stale and overwritten four seconds later. One database cannot take that write rate, and no bigger database fixes the mismatch, because the mismatch is that you are writing to disk at all.
The second failure is the read. The rider's SELECT scans the drivers table looking for nearby rows. With a million rows, a filter on a bounding box still has to examine far too much of the table for every rider who opens the app, and the reads pile up on top of the write firehose on the very same rows. A plain table has no notion of "near"; it can only look at rows one after another. The third failure is that everything lives in one box: when it dies, every driver's location is gone at once, every rider's query fails, and no trip can be created. Those three failures point the same way — the location data has to come off disk and spread across many machines, the read has to stop being a scan, and the durable thing (the trip) has to be kept apart from the fleeting thing (the position). The rest of this design does exactly that, one forced decision at a time.
part 2 The version that survives scale
This section walks the design by use case, in the order you actually have to decide things. First, get the shape right — a front door and a tier of workers that hold positions — and see the naive full scan that shape still has. Then face the write firehose head-on and decide where a million positions a second are even allowed to live. Then make "who's nearby" a bounded query with a spatial index, and get the geometry of that index right. Then make sure one driver goes to exactly one trip, without a cluster-wide lock, and survive a worker dying. And finally survive a demand flood, which turns out to need the opposite of the obvious fix. The finished diagram waits at the end.
Two ways through this section
The five decisions below are the heart of this design, and there are two ways to take them. In the design room you make each one yourself and the board reacts to your pick. The write-ups after this point cover the same decisions in full — read them instead, or afterwards as reference.
use case 1 A driver pings, a rider sees cars nearby
On day one, one box did everything: took the traffic, held the driver positions, kept the trips. That cannot grow, so the first move is to break it into parts that scale independently. Doing so also exposes the naive full scan this shape still leans on to answer a rider.
deep dive 1 The pipeline and the full scan
The question
The single-server version put the front door, the position store, and the trip store all in one box. The first move is to pull them apart into a shape that can grow. Two roles are obviously different. One role is receiving traffic — accepting a flood of driver pings and a stream of rider queries and requests. The other role is holding driver positions and answering "who's nearby." Splitting them lets each scale on its own. A driver's ping and a rider's "who's nearby" arrive at very different rates and for very different reasons — so where does each land, and what holds the positions the rider query reads?
The location stream is an inlet, not a connection design, and it is worth saying why once and moving on. The driver's app streams a ping every few seconds over a long-lived connection. Everything about that connection — how it is kept open across a mobile network, how millions of them are balanced across gateway nodes, how a gateway restart hands them off without dropping pings — is the persistent-connection problem the chat board owns. Re-deriving it here would rebuild another board's signature, so the gateway takes a ping as an inlet and the connection layer underneath is out of scope.
That leaves the real shape decision, which is about where state is allowed to live. The gateway must hold nothing, because it sits on the firehose: a million pings a second flow through it, and anything it had to remember per driver would pin that state to the one tier you most need to keep thin and replaceable. Push all the position-holding back to the geo workers, and the gateway becomes a pure forwarder you scale flat. The workers hold the state and do the finding — which is why the two hard problems that follow, the firehose write and the nearby read, both land on the workers and nowhere else.
A stateless Location gateway in front, a tier of geo workers holding positions behind it. Put a thin, stateless Location gateway in front, and a tier of geo workers holding positions behind it. The pings arrive over a connection the driver's app holds open — that connection itself is not what we are designing here; it is an inlet, so a ping shows up at the front door and we take it from there. The gateway accepts a ping and hands it onward, accepts a rider query and routes it, and holds no driver state of its own, so it scales by simply adding more stateless instances. Behind it, the geo workers hold driver positions, split across many of them, and answer the rider's "who's nearby." A ping is forwarded to a worker, which records the driver's latest position; a nearby query is forwarded to the workers, which look for drivers close to the rider.
And right now, established here as the baseline the next use cases attack, the way a worker answers "who's nearby" is a full scan: it looks at all the positions it holds and keeps the ones near the rider. That works, and it is honest to draw it, but it is the naive baseline on two axes at once. The positions are still being written the expensive durable way inherited from the single-server design, and the read is still a scan. The next two use cases fix exactly those two things, in that order — first the firehose write, then the nearby read.
Under the hood — how it works
The Location gateway is stateless and holds only routing information, so it scales flat: a million pings a second is a lot of forwarding but no per-ping work, and you add gateway instances until the forwarding keeps up. If a gateway instance drops a ping, the cost is tiny — the same driver pings again in about four seconds, so a lost ping is a position that is a few seconds stale for a few seconds. That fire-and-forget tolerance is the first hint of the idea the whole design turns on: the firehose re-sends, so a little loss is free.
The geo workers, at this stage, are sized the naive way and it does not add up yet: durable writes on the firehose and full-scan reads. That is the tension the next two use cases resolve, and it is why this use case only establishes the shape rather than declaring victory. Nothing here is the marquee decision — it is the pipeline the marquee decisions rewire.
⚡ From production
This is not an interview simplification. Uber's real-time platform reported that drivers send a location update every four seconds as they move around, and named its design goal outright: handle a million writes per second. That single cadence is what turns "track some drivers" into a systems problem instead of a CRUD app — and it is why the front door has to be a stateless forwarder rather than anything that remembers a driver between pings.
The board after this deep dive — “A driver's position lands” traced, hop by hop. The numbered steps below walk the same path.
A driver's position lands
the driver's app streams a ping to the stateless gateway — fire-and-forget, no wait~1 ms
the gateway forwards the ping to a geo worker, which records the driver's latest position (still the naive durable way, for now)~2 ms
use case 2 A million writes a second
So far the geo workers keep positions on disk, exactly as the old single box did. That was fine for a trickle. This use case finally aims the real firehose at them — the write rate the brief promised — and disk is about to stop being an option.
deep dive 2 A million writes a second
The question
The pipeline has a shape now, but the geo workers are still storing positions the way the single server did: durably, on disk. The firehose from the brief is now pointed straight at them, and this is where it either sinks the design or forces the idea the whole system is built on. A quarter of a million location writes arrive every second in one region, and each one is a driver's new position that will be stale in four seconds. Where does that write land?
Follow the obvious answer first, because it is what most people reach for. Keep a drivers table — now maybe sharded across several databases so no single one takes the whole rate — and UPDATE the driver's row on every ping. The attraction is real: it is the design you already have, it is simple, and the position is safely on disk if anything crashes. But size it. Each UPDATE is a durable write, about ten milliseconds of work to put the new position on disk in a crash-proof way. You are doing that a quarter of a million times a second in one region, and every single write is durably saving a number that a fresh ping overwrites four seconds later. You are paying the full cost of durability — the slowest, most expensive property a store can offer — for the one piece of data in this whole system that has no durability requirement at all. Sharding the table spreads the pain across more disks, but it does not remove it; you are still writing to disk on the firehose.
The second attempt improves the medium but keeps the mistake: put the positions in a fast store like Redis instead of a relational table — memory-speed writes, much higher throughput. That is genuinely faster. But if you configure it to persist every write for durability, you are back to paying for durability the data does not need, now on a different box. The store being fast is not the point; the point is whether the write is durable at all, and a persisted position write is still a durable write on the firehose. Only when you see that the firehose itself makes durability unnecessary does the cheap answer — memory, no persistence — become the safe one.
Live location in memory across the geo workers — non-durable, no database. Live driver location never touches a database — it lives in memory across the geo workers, and it is not durable. Each geo worker holds its slice of driver positions in RAM. A ping updates a value in memory, which takes microseconds instead of ten milliseconds, and nothing is written to disk. The design deliberately accepts that these positions are not crash-proof — and the reason it can accept that is the firehose itself. A position is only ever a few seconds from being replaced: if a worker loses a position, because the process restarted or a whole worker died, the driver's next ping, at most four seconds away, writes it again. The data heals itself. There is nothing to recover because the source keeps re-sending. Uber states this exact choice in its own words: database storage would be useless because of how fleeting the location data is.
That is the aha, and it inverts the naive instinct. The firehose is not a scaling problem you throw a bigger database at — the firehose is the reason you can drop durability entirely: data that rewrites itself every four seconds does not need to survive a crash, so it does not need a disk, so it does not need a database. But durability did not vanish; it moved. A trip is not fleeting. "This rider took this ride with this driver, and it cost this much" has to survive crashes, be queried later, and be exactly right. So the design splits the two datasets by their real needs: live positions get freshness without durability, held in memory on the geo workers; trips get durability, in a separate store built for it — the Trip store, a relational store sharded a fixed number of ways. Durability is spent on the one dataset that needs it and denied to the one that does not. That split — freshness here, durability there — is the signature of this whole design.
Under the hood — how it works
The geo workers are sized by throughput, and this is the sizing that forces the box to split. A region's writes a second — and the fleet-wide goal across regions — divided by how many in-memory position updates one worker process can absorb, gives the number of workers. It is the write rate, not the amount of data, that sets the count. And here is the second sizing, which lands on the same box and comes out tiny: a single position is an id, a latitude and longitude, and a timestamp — about fifty bytes. A million of them is about fifty megabytes, which fits in the memory of one node many times over. So the data would happily live on a single machine; it is the write throughput that forces the fleet. Throughput splits this tier into many workers, while capacity would have left it on one — the two sizings pin the same box in opposite directions, which is the whole reason you compute both.
The Trip store · MySQL is sized the opposite way. Its writes are rare next to the firehose — one write per trip lifecycle event, when a trip is requested, accepted, started, and completed — orders of magnitude below the ping rate. Its capacity grows with trip history, not with the live fleet, and it is sharded a fixed number of ways to spread that history. It is also the record the product reads back: the rider's app polling a trip's status mid-ride, and the trip history both apps show later — indexed reads by trip id, at rates nowhere near the firehose. Its failure story is the serious one: if a shard is lost, durable trips are lost, so this store must be replicated and backed up. That is precisely the point of keeping it separate — the expensive, careful durability machinery is spent here, on the data that would be a real loss, and not wasted on positions that rewrite themselves.
⚡ From production
The "database storage would be useless" line is Uber's own, from its engineering writeup on Ringpop, which describes live driver location held in-memory across a fleet of geospatial worker processes — while the durable trip data lives in a separate system entirely, Schemaless over MySQL, split into a fixed count of shards. The freshness-versus-durability split is not an interview trick; it is how the production system is actually partitioned.
The board after this deep dive — “A driver's position lands” traced, hop by hop. The numbered steps below walk the same path.
A driver's position lands
the driver's app streams a ping to the stateless gateway — fire-and-forget, no wait~1 ms
the gateway forwards the ping to a geo worker, which updates the position in RAM in microseconds — nothing goes to disk; a lost position is rewritten by the next ping~1 ms
use case 3 Drivers near me
Writing is solved — positions pour into memory. Reading is not: the only way a worker can find the cars near a rider today is to walk its whole list of positions. The write got its fix last time; the read gets its turn now, and the crucial call is the one most people make backwards.
deep dive 3 Drivers near me
The question
The positions are in memory now, fast to write. But the read is still the naive full scan from the first use case: to answer "who's near this rider," a worker looks at every position it holds. The geo workers hold a region's million live positions between them; a rider opens the app at one point on the map and wants the cars within a couple of kilometers. How do you answer that without looking at every position? The firehose fixed the write; this use case fixes the read.
The instinct is to argue about granularity — "use a finer grid so cells are smaller and queries are more precise" — and seeing why that is a trap is the heart of this deep dive. All the serious encodings — geohash, S2, H3 — let you pick essentially any cell size you want. Uber's original S2 index used level-twelve cells at roughly three to six square kilometers, which sits right between two H3 resolutions; H3's resolution knob alone spans about thirteen orders of magnitude in area across its sixteen resolutions, from hexagons of millions of square kilometers down to under a square meter. Granularity is a knob every option has. It was never the thing that separated them.
Walk the candidates by geometry instead. Geohash encodes a latitude-longitude pair into a string, and its cells are rectangles whose ground area changes sharply with latitude — a grid whose cells quietly change size as you move around the globe is awkward to reason about. S2, Google's library and Uber's original choice, is better: it projects the sphere off the six faces of a cube and recursively splits each face into four, so it is literally a quadtree on the sphere. Its cells are squares — and squares are where the subtle problem lives, because a square has two neighbor distances, edge and corner, so any analysis over a neighborhood keeps correcting for which kind of neighbor it is looking at. H3 tiles the map with hexagons, which have one neighbor distance, and that is the whole reason Uber built it. The migration from S2 to H3 was not about smaller cells; it was about swapping a shape with two neighbor distances for a shape with one.
A hexagonal spatial index (H3), cell resolution tuned to driver density. Bucket driver positions into hexagonal cells, and to answer a nearby query, scan the rider's cell and its ring of neighbors. The idea behind every answer is the same: stop treating positions as an unordered pile and start bucketing them by where they are, so a query that used to scan the whole region now scans a handful of cells. The pick between the encodings is not about granularity — all of them let you choose essentially any cell size — it is about geometry, the shape of the cells. A square cell has two different distances to its neighbors: the four cells across an edge are one step away, but the four across a corner are farther, about one and a half steps. A hexagon has one neighbor distance — all six neighbors are the same distance from the center — so "the neighboring cells" is one clean ring with nothing to correct for. In Uber's own words, hexagons have only one distance between a centerpoint and its neighbors, and that property greatly simplifies analysis and smoothing over gradients. That, not smaller cells, is why Uber moved from squares to hexagons.
The positions themselves are spread across the whole worker fleet, so a nearby query fans out: each worker scans the rider's cell and its neighbors among the positions it holds and sends back its shortlist, and the gateway merges the handful of shortlists into one answer. The read that was a full scan of the region becomes a scan of a small, fixed neighborhood. The one genuine knob left is the cell resolution, tuned to the driver density you actually see. And one myth to kill on the way past: Uber's live driver index was never geohash. It was S2, and then H3. Geohash shows up here only in supporting roles — the internal encoding inside Redis's geospatial type, and an offline aggregation key in Lyft's ETA model. Saying "Uber uses geohash for its live index" is wrong, historically and today.
Under the hood — how it works
The spatial index adds no box and no node count — it is the data structure the geo workers already hold their positions in, so its "size" is the same working set from the last use case, now organized by cell instead of piled up. What it adds is a parameter: the cell resolution, and that parameter is where the failure mode lives. Pick it too coarse and you get hot cells — at rush hour a single downtown cell can hold thousands of drivers, so a query into it is nearly a full scan again; pick it too fine and an ordinary radius query spans a hundred cells. There is no clean sourced incident for this — it is an honest tuning tension, not a war story — and real systems tune resolution, sometimes varying it by area, to keep any one cell's population bounded.
Before you have a million drivers, you do not need a custom worker fleet at all. Redis ships a geospatial type that is really just a sorted set: it interleaves a position's latitude and longitude bits into a single integer — a geohash — and uses it as the sort score, so a radius search becomes a range query over scores, with a worst-case distance error under half a percent. Adding a position costs logarithmic time in the set's size, and a search costs time proportional to the points in the area it scans. That is a completely reasonable index for a city-sized deployment, and it is exactly the sort of single-node structure the fleet-scale in-memory index has to beat once the fleet outgrows one box. Naming it keeps the dive honest: the fancy answer is for the fleet, and there is a simple, real answer for everything smaller.
⚡ From production
The one-neighbor-distance property is Uber's own stated reason for building H3, and the same writeup names a second win: H3 replaced hand-drawn operations zones that required frequent updating as cities changed. The lineage is S2 (a quadtree of square cells) to H3 (hexagons) — geometry, not granularity, was the axis. The resolution knob spans about thirteen orders of magnitude in area, which is exactly why granularity was never the thing that separated the encodings.
The board after this deep dive — “A rider sees who's nearby” traced, hop by hop. The numbered steps below walk the same path.
A rider sees who's nearby
the rider asks who's nearby; the gateway routes the query~1 ms
the gateway fans the query to the workers; each scans the rider's H3 cell and its neighbor ring among the positions it holds — a bounded nearby-cell scan, never the whole fleet — and the gateway merges the shortlists~3 ms
use case 4 One driver, one trip
By now the board looks solid: writes land in memory instantly, and a nearby lookup is cheap. So this use case goes looking for the breaks — and finds three of them, hiding behind a single question about which worker is in charge of a given driver.
deep dive 4 One driver, one trip
The question
The board reads well now: positions land in memory at firehose speed, and a rider's nearby query is a bounded scan. Time to try to break it. Two things the happy path glossed are about to bite, and they are really one thing. A ride request has to reach the exact worker holding the target driver's live state — and two riders must never both be told they got the same driver. And a worker holding a slice of the fleet will, at some point, die. Both come down to one question: who owns a given driver's state, and how is that ownership decided and moved — without a lock across the whole cluster?
The instinct is a distributed lock: before creating a trip, take a cluster-wide lock on that driver's id so no one else can grab them, and release it when the trip is created or the offer is declined. It sounds right and it is a trap. A lock negotiated across a cluster on every dispatch is slow — it is a round trip to a lock service on the critical path of every ride — and it adds a new thing that can fail and stall every dispatch behind it. Worse, it is solving a problem the design can avoid entirely rather than manage.
The real answer makes the double-booking question local by deciding ownership up front, so there is nothing to coordinate. Route every request for a driver to that driver's one owning worker by hashing the driver id, and the worker serializes them in its own process — the first books, the second is declined. No lock service, no round trip, no new failure mode on the critical path. The guarantee falls out of the routing: one owner per driver means one place makes the decision, in order.
A consistent-hash ring — one owning worker per driver, decided by hash(driverId). Put the geo workers on a consistent-hash ring, and make the double-booking question local by deciding ownership up front. Hash each driver's id onto the ring, and the ring assigns that driver to exactly one owning worker. Now every request that touches a given driver — a position update, a dispatch offer — is routed to that one owner by hashing the driver id, with no directory consult, because the hash is the address. And because all requests for a driver land on the same single worker, "don't double-book" stops being a distributed problem: that one worker handles its drivers' requests one at a time, in its own process. Two ride requests for the same driver both route to the same owner, which handles them in order — the first finds the driver free and books them, the second finds the driver taken and is declined and offered the next candidate. It is a local decision inside one process — no cluster-wide lock, because there is nothing to coordinate across the cluster.
The same ring is the failure story, and it is why the index can be non-durable. A consistent-hash ring is built to lose a node gracefully: when a worker dies, the ring hands its slice of drivers to the neighbor around the ring — in Uber's words, when node one in the hash ring goes down, work proceeds as usual, and all of those objects between nodes one and two get picked up by node two. The dead worker's in-memory positions are gone, but that is exactly the loss the signature dive already made safe: every driver on that slice pings again within four seconds, and the ping lands on the new owner, which records the position. The slice re-populates itself in about one ping cycle. This closes the loop opened two use cases back — the geo workers can afford to hold positions in memory with no durability precisely because the ring plus the firehose rebuild a lost worker's state automatically.
Under the hood — how it works
The Dispatch service is stateless — the durable state of a trip lives in the Trip store, and the live state of a driver lives on the owning geo worker. It handles one dispatch per ride request, which is orders of magnitude below the ping firehose: a rider requests a ride occasionally, while a driver pings every four seconds. So it scales flat by adding instances. Its failure story is the double-book case handled above — two requests for one driver serialize at that driver's single owning worker, so a dispatch instance dying mid-request loses at most that one in-flight offer, which the rider's app retries. The offer reaches the driver's phone the same way their pings arrive: back over the connection this design treats as an inlet — an actor link, not a box edge.
Once dispatch has the nearby drivers in hand, choosing which one to offer is its own problem, and a real one. Uber does not greedily pick the closest driver; it evaluates nearby riders and drivers together in a batch and pairs them to reduce everyone's wait, because closest does not always mean quickest, and it filters on hard rules first — it will not re-match two people who once gave each other a one-star rating. That is an assignment-optimization algorithm, a different kind of problem from the systems routing this board teaches. We name it and route it to a future stepper rather than design it here.
⚡ From production
Uber's Ringpop library shards in-memory state with a consistent-hash ring and SWIM gossip membership, with no central coordinator: nodes discover and ping each other to track who is alive, and the node-one-to-node-two handoff is its built-in failure behaviour — when node one goes down, the objects between nodes one and two are picked up by node two. The double-booking guarantee is a routing property (one owner per driver), not a distributed lock.
The board after this deep dive — “A rider requests a ride” traced, hop by hop. The numbered steps below walk the same path.
A rider requests a ride
a rider requests a ride; the gateway routes it~1 ms
forwarded to the Dispatch service~1 ms
dispatch asks the geo workers for nearby drivers — a bounded query out, a ranked list back; the offer to the chosen driver's owning worker rides this same edge (by the ring)~3 ms
on acceptance, dispatch writes the trip to the durable Trip store — the one durable write on the ride path~10 ms
use case 5 When demand floods the marketplace
For a normal, heavily-used city, the design is genuinely finished. This last use case breaks it anyway — and fairly, because nothing built so far is at fault. Every tier does its job, and the service still collapses. The answer, once again, runs against the instinct to add machines.
deep dive 5 When demand floods the marketplace
The question
Step back and look at what is built. It is correct and complete for an ordinary busy city: the firehose lands in memory, nearby is a bounded query, and one driver goes to one trip while surviving a worker's death. Now the premise arrives already broken, the way a real incident does — and it is honest to say up front that this one is not a failure of anything built so far. The location pipeline is fine. The index is fine. The dispatch routing is fine. The system falls over anyway, and understanding why is the point. It is New Year's Eve. Across a city, far more people want a ride than there are drivers to give one, all at once. On a real night like this, Uber saw a 26-minute window where its surge-pricing algorithm was down in New York; with that one signal missing, the share of ride requests that went unfulfilled rose to 25% and estimated waits spiked to about 8 minutes — against a 2.6-minute average during a comparable event where surge worked. Nothing in the location or dispatch tier had crashed. The thing that broke was the marketplace's balancing signal.
The instinct is to add capacity — spin up more geo workers, more dispatch instances, throw hardware at it. It is the reflex that worked when the firehose needed more workers, and here it does nothing, because the shortage is not compute. There are only so many drivers on the road. A thousand more dispatch instances cannot match a rider to a driver who does not exist. When demand far exceeds supply, the bottleneck is the supply of cars, and no server you add creates a car.
So the only responses that help are the ones that work on the side you control: reach further out for a driver, hold and admit requests as drivers free up, and tell riders the honest wait — while the marketplace's balancing signal does the actual rebalancing over time. The New Year's Eve outage is the clean proof: nothing in the location or dispatch tier crashed, and the marketplace still fell over, because the balancing signal — not any box on this board — is what keeps supply and demand in step.
Degrade gracefully — widen the radius, shed or queue, quote honest ETAs. Degrade gracefully on the dispatch path — never pretend the index can conjure supply. When a region floods and there are not enough nearby drivers, the dispatch service has real, honest moves, and none of them is "scale up." It can widen the search radius — reach further out for a driver, accepting a longer pickup, rather than returning nothing. It can shed or queue requests — hold new requests briefly and admit them as drivers free up, instead of failing every one under load. And it can quote honest, longer ETAs — tell the rider the real wait rather than a comfortable lie. Underneath all three, the marketplace's own balancing signal — surge pricing, which this board does not design — is what actually rebalances supply and demand over time by drawing more drivers out and damping some demand. The New Year's Eve incident is the proof of what happens when that signal is missing: unfulfilled requests and long waits, not because the machinery broke, but because the loop that keeps supply and demand in balance went quiet.
The reframe is the lesson to carry out, and it is bigger than this one board. A ride-sharing service is not a request-response system that either has capacity or does not. It is a control loop between supply and demand, and matching is worthless when there is no supply to match. You can build a flawless location firehose, a perfect spatial index, and lock-free dispatch — everything in the four use cases before this — and the system can still fall over under a flood, because the thing that fails is not any component but the balance between two sides of a market. The engineering answer to a flood is to degrade honestly on the side you control, and to respect that the real fix lives in the balancing signal, not in more servers. The hot-cell tension from the spatial-index dive is this same flood in miniature — one part of the map carrying far more load than the rest, answered by adapting to the skew rather than pretending one setting fits every load.
Under the hood — how it works
Graceful degradation is a policy on the dispatch path, not a component, which is why it is a badge and not a box. Widening the radius trades a longer pickup for an answer instead of a blank; shedding or queuing trades a brief hold for not failing every request at once; honest ETAs trade a comfortable lie for a number the rider can plan around. All three keep the system answering under load. The one thing none of them does — and the one thing the instinct reaches for — is manufacture a car, which is why "add capacity" is the wrong verb for this failure. The real rebalancing lives in surge pricing, the ranking/ML core this board deliberately does not build; here it is named as the marketplace signal behind the incident, and pointed at, not designed.
⚡ From production
Every number here is Uber's own, from its published writeup of the New Year's Eve outage: a surge-pricing outage in New York pushed unfulfilled requests up and estimated waits to several minutes, against a much shorter average during a comparable high-demand event where surge worked normally. The comparison point is what makes the incident legible as a marketplace-balancing failure rather than a capacity one — which is precisely why it teaches that a ride-sharing service is a control loop, and matching cannot manufacture supply.
The board after this deep dive — “A rider requests a ride” traced, hop by hop. The numbered steps below walk the same path.
A rider requests a ride
a rider requests a ride; the gateway routes it~1 ms
forwarded to the Dispatch service~1 ms
dispatch asks the geo workers for nearby drivers — a bounded query out, a ranked list back; the offer to the chosen driver's owning worker rides this same edge (by the ring)~3 ms
on acceptance, dispatch writes the trip to the durable Trip store — the one durable write on the ride path~10 ms
Where this design sits on consistency (and CAP)
This design makes opposite consistency choices for its two datasets, on purpose, because the two datasets are worth completely different things. The live driver location chooses availability and freshness over everything else. A position is held in memory, non-durable, and answered from whatever the geo worker knows right now; if a worker is unreachable or a position is briefly missing, the system does not stop and coordinate to be exactly right — it answers with what it has and lets the driver's next ping, at most four seconds away, correct it. A slightly stale or briefly missing car on the map is a small, self-healing harm, so the live index takes availability every time.
The trip record makes the other choice. "This rider took this ride with this driver, and it cost this much" has to be durable, queried later, and exactly right — a lost or double-created trip is a real, unrecoverable harm, not a harm the next ping repairs. So the Trip store is the CP-leaning dataset: it lives in a replicated relational store, and the one place two riders could race for the same driver is resolved before a trip is ever written, by routing both requests to that driver's single owning worker so the decision is made in one place, in order. Correctness is bought where it matters and declined where it does not.
Read the whole board that way and the split is the design. Freshness without durability for location, because the firehose rewrites it; durability with coordination for trips, because nothing rewrites a trip. The consistent-hash ring is what lets the live index be available and still avoid a double-book — a lost worker's slice hands off to a neighbor and re-populates in one ping cycle, so availability never costs a trip.
There is a name for the underlying choice. When a network fault splits a system in two — that split is the P, for partition — each operation has exactly two honest options: answer anyway from what this half knows and risk being wrong, which is the A, for availability, or refuse until the halves agree, which is the C, for consistency. A partition is not something you choose; it is weather. The useful way to apply the CAP theorem is per operation, by asking what a wrong answer would cost. Here a stale car on the map costs almost nothing while a lost or duplicated trip costs everything, so the design takes availability for the live index and pays for consistency only on the trip record.
That is the whole machine, and every box on it was placed by one of the deep dives above. A million writes a second land in memory and self-heal without a database in sight; a rider's "who's nearby" is a small cell scan, not a fleet scan; one driver goes to one trip without a cluster-wide lock, and a dead worker's slice hands off to its neighbor and refills in one ping cycle; and under a flood the marketplace degrades honestly instead of pretending it can conjure drivers. If you carry away one sentence, make it the one the whole design turned on: not all data deserves durability — match the storage guarantee to how long the data is worth keeping.
The finished design — every box placed by one of the deep dives above.
What's on the diagram
Driver
Driver. The driver's app, streaming a location ping to the front door roughly every four seconds — the firehose. It is fire-and-forget: it reports its position and waits for nothing, because the next ping is only seconds away, which is exactly why a lost ping costs almost nothing.
Rider
Rider. The rider's app — it asks who is nearby to draw the map, and requests a ride when the rider taps. The nearby query stays bounded to a small patch of the map; the ride request is the one call that creates durable state.
Location gateway
Location gateway. The thin, stateless front door: it accepts fire-and-forget driver pings and rider queries and requests and forwards them, holding no driver state of its own, so it scales flat by adding instances. The location-stream connection underneath it is treated as an inlet — the chat board's problem, not this one's.
Geo workers · in-memory
Geo workers · in-memory. The fleet that holds live driver positions in RAM, non-durable — a ping updates a value in microseconds and never touches disk. Positions bucket into an H3 hexagonal spatial index for bounded nearby scans, and the tier is a consistent-hash ring that gives each driver one owning worker and hands a dead worker's slice to its neighbor.
Trip store · MySQL
Trip store · MySQL. The one durable dataset: the trip record and its state machine, from requested through accepted, started, and completed. It is sharded a fixed number of ways and replicated, because a lost or double-created trip is a real, unrecoverable harm — unlike a position, which the next ping rewrites.
Dispatch service
Dispatch service. Stateless: it takes a ride request, runs a bounded nearby query against the geo workers, routes the offer to the driver's owning worker by the ring — no cluster-wide lock — and writes the trip to the durable store on acceptance. Under a flood it degrades honestly, because matching cannot conjure a car that is not there.
What this design never covered — raise it yourself
The matching algorithm — which nearby driver wins the offer
A strong follow-up is: you found the nearby drivers, now which one gets the ride? The honest answer names it as an optimization core this board did not build — real systems evaluate nearby riders and drivers in a batch and pair them to reduce everyone's wait, filtering on hard business rules first, because closest is not always quickest. Name it as the one level deeper, and point at a matching stepper rather than inventing an assignment algorithm on the spot.
Turn-by-turn routing and ETA
Once a driver is matched, computing the fastest road path and a real arrival time is a graph problem over a road network, traffic-aware — a separate system entirely. Name it as the cut it is; it does not touch the location or dispatch boxes.
Surge pricing and demand forecasting
The deepest version of the flood use case is the pricing model that actually rebalances supply and demand — a modeling and machine-learning core. The strong answer names it as the marketplace's balancing signal that sits outside this board, and points to the New Year's Eve outage as the evidence for why it matters, without pretending to design it.
The driver location-stream transport
This design took the ping as an inlet. The follow-up is how millions of phones hold connections open, balance across gateway nodes, and survive a gateway restart without dropping pings — which is the persistent-connection fabric the chat board solves. Name it and reach for that board, rather than re-deriving a connection layer here.
Go deeper — the primitives this design leaned on
Quadtree ↗ — the spatial subdivision that turns "find things near me" from a full scan into a bounded query over nearby cells; S2, Uber's original live index, is a literal quadtree
Consistent hashing ↗ — the ring that gives each driver one owning worker and reshuffles only a dead node's neighborhood — both the no-double-book routing and the failure handoff