InterviewLegend

Design Uber (Ride Matching)

Signature problem
Alex Chen45 min readUpdated 548 real interview reports
In this lesson
  • Scope ride matching around a location firehose and a nearest-driver query that returns in milliseconds
  • Start from how the map is partitioned into cells, before drawing any diagram
  • Work the geospatial index, firehose ingestion, and matching without double-booking a driver

Understanding the problem

"Design Uber" is the most Uber-shaped design prompt there is, because it is Uber's own core system. Picture the job from the rider's seat. Someone opens the app. Within a second or two they see little cars scattered on the map around them. They tap a button to request a ride, and a few seconds later a specific driver is on the way. Now picture it from the driver's seat. Their phone streams its location up to Uber every few seconds, the whole time they are online. The system has to take that endless stream of "where is every driver right now." Then, for any rider who asks, it answers "which available drivers are closest to this exact spot," fast enough that the tap-to-match feels instant. Almost every hard part of this design is that geospatial work at enormous scale. It is the math of locations on a map.

Anchor on scale before drawing anything, because the scale is what makes this hard. Uber has millions of active drivers and millions of active riders at peak. Each driver's phone reports its location roughly every 4 seconds while online. Do that arithmetic. Millions of drivers, each writing a new location every 4 seconds, is on the order of a million location updates every second flowing into the system. That is a write firehose. It is a stream of writes so large and so constant that it dwarfs almost everything else the system does. On the other side sits the matching request: rare by comparison, but extremely latency-sensitive. A rider expects to see nearby cars and get matched within a few seconds. They will not wait minutes. So the system has two very different headline problems bolted together. Absorb a torrent of writes, and answer a sharp nearest-neighbor query against that constantly-moving data right now.

One more property shapes everything. This is a geography problem, so it partitions by geography. A rider in London never needs to be matched with a driver in Tokyo, and a query for nearby drivers only ever cares about a tiny patch of the planet. That means the system can be split by region, with each city or area running largely on its own. That is good for scale, since no single machine holds the whole world. It is good for availability too, since an outage in one region must not take down the others. Riders and drivers in unaffected cities should never even notice. Three facts hold together here. A write firehose. A latency-critical geospatial query. Natural partitioning by region. Together they turn "match a rider to a car" into a serious distributed systems problem.

The Uber angle
The person across the table may have shipped this system. "Put the drivers in a database and query for nearby ones" will not survive them. Two things carry a strong answer here. First, the location firehose and the "find nearby drivers" query are the headline, not afterthoughts under a box labeled database. Build the design around them. Second, have a real story for concurrency: when two riders both want the one driver between them, exactly one gets the car, and the other does not silently get a phantom match. The interviewer pushes hardest on two specifics, the geospatial index you pick and why, and the dispatch protocol that offers a driver and locks them against a double-match. Name both early, then build toward them.

Functional requirements

Propose a tight scope and leave the interviewer room to steer. The core is small. Drivers publish their location continuously. A rider requests a ride from a location, and the system finds the nearby available drivers. The system matches that rider to exactly one driver. And it tracks the resulting trip through its lifecycle until it completes. Everything else can be named and set aside.

In scope

  • Drivers publish their location continuously while they are online
  • A rider requests a ride from a location, and the system finds nearby available drivers
  • Match the rider to exactly one driver, with no double-booking
  • Track the trip through its lifecycle (requested, matched, en route, in trip, completed)

Below the line (call it out, then skip)

  • Payments, fares, and tipping (a separate billing system with its own design)
  • The maps and routing engine itself (we consume an ETA from it; building the road-network router is its own problem)
  • Ratings and reviews of drivers and riders
  • Uber Eats, pooled rides, and fraud detection

Non-functional requirements

These are where the design is won, so reason through each with a target. Matching has to be low-latency end to end, on the order of a few seconds from the rider tapping the button to a driver being assigned. A rider staring at a spinner abandons quickly. Location ingestion has to sustain very high write throughput, around a million writes per second at peak, every second, all day. The system has to be highly available, and specifically it has to fail in a contained way: a region going down can degrade that region, but it must never cascade into other regions. Location data is allowed to be eventually consistent. A driver's pin can lag their true position by a second or two and that is fine, since a car that has moved twenty meters is still a fine match. The one place strong consistency is non-negotiable is driver status. A driver must never be offered to or matched with two riders at the same time.

One tension is worth flagging now and returning to later. Location wants to be cheap and fast, and it tolerates being eventually consistent, since it changes constantly and a slightly stale position does no harm. The match wants to be strongly consistent, since double-booking a driver is a real failure a rider feels in the world. Those two requirements pull in opposite directions. The design resolves them by treating live location and the match as two different kinds of data with two different storage strategies. The line that does not move is no double-match. Stale location is acceptable. A driver assigned to two trips at once is the failure this whole system exists to prevent.

  • Active drivers: ~Millions. Plus millions of active riders at peak; each driver online and pinging the whole time they work
  • Location pings: ~1 every 4s/driver. On the order of a million location writes per second; the system is overwhelmingly write-heavy
  • The real metric: Nearby-query latency. Find the closest available drivers in milliseconds, and never double-match one driver
  • Partitioning: By geography. Split by region so no machine holds the world and one region's outage cannot sink the others

The set-up

Core entities

Name the handful of objects the system moves around before drawing anything. Five carry the design:

1

Driver

One driver, with an id, a current location (latitude and longitude), and a status that is either available, offered, or on-trip. The location updates every few seconds. The status is the field that must stay strongly consistent, because it is what decides whether this driver can be matched right now.

2

Rider

One rider, with an id and a current location. A rider is far less chatty than a driver. They appear when they want a ride and ask the system to find them a car.

3

Geo-index

The live, in-memory structure that maps map cells to the drivers currently in them. A map cell is a small tile of the map, a patch of the world the index can look up directly. This index is what answers "which drivers are near this point" without scanning every driver on Earth. It is the heart of the design.

4

Trip

The record of one ride and, more importantly, a state machine: a small set of allowed states with rules for how you move between them. A trip moves requested, then matched, then en route, then in trip, then completed. Encoding it as a state machine is what stops a trip from ever being in two states at once or skipping a step.

5

Match

The act of binding one rider to one driver for one trip. The match is the strongly-consistent moment in an otherwise loose, eventually-consistent system. It is where the double-booking problem lives.

One split runs through the rest of the design, so make it explicit now. Live location is hot and write-heavy and disposable. It changes every few seconds and is read only to answer nearby queries. Last second's value is worthless the moment a new one arrives. So it lives in memory and is never durably written on every ping. The match and the trip are the opposite. They are written rarely, they must survive a crash, and they demand strong consistency, so they live in a durable database. Driver status sits between the two and leans toward the strong side. It changes rarely, but a wrong value double-books a driver. So it is guarded by the matching layer rather than left to the loose location store. Keep "hot and loose" separate from "rare and strict." That is what lets one system carry both a write firehose and a no-double-match guarantee at once.

API

The API has two distinct shapes, one for each side of the marketplace. Drivers do almost nothing but write: they stream their location up over and over. Riders do almost nothing but ask once: they request a ride and then watch the trip progress. The driver app also keeps a persistent connection open. This network connection stays alive so the server can push to the driver at any moment. The server needs to send a dispatch offer the instant a match is found, rather than wait for the driver to ask. That persistent connection is a WebSocket, a two-way channel held open between the app and the server.

The interface
RequestServer pushFire-and-forget
1
driverserverreport my position, the firehose, called every ~4s
updateLocation(driverId, lat, lng)
{ ok }
no durable write per ping
2
riderserverrequest a ride from a point, get back a match
requestRide(riderId, lat, lng)
{ tripId, driver, etaSeconds }
resolves once a driver accepts
3
serverdrivera dispatch offer is pushed down the open WebSocket
onOffer
{ tripId, pickup, etaSeconds, expiresInSeconds }
4
driverserveraccept the offer you were just pushed
acceptRide(driverId, tripId)
{ ok, trip }
the match commits here, atomically

Two details carry the protocol. The updateLocation call is fire-and-forget and does not durably write per ping. Writing a million rows a second to a disk-backed database would be both impossible and pointless, since the value is overwritten four seconds later. The dispatch offer is pushed from the server to the driver over the open WebSocket rather than pulled. A driver cannot poll fast enough to make matching feel instant. The whole point is for the server to reach a chosen driver the moment it picks them. The acceptRide call is the atomic commit point of the entire system. It is the single instant where one driver is bound to one trip, and the deep dives turn it into the heart of the design.

High-level design

Walk the system one functional requirement at a time and build the simplest thing that works before optimizing. Two flows run side by side. A write side has drivers stream location into the live geo-index. A read side has a rider request flow through the gateway to the matching service and back. In the diagram, color marks each role. Blue marks the rider and driver apps. Violet marks the gateway and matching service. Amber marks the location-ingestion path. Green marks the geo-index and trip store.

Loading diagram...

Here is how the pieces connect. On the write side, the driver app streams its position to a Location service, an ingestion pipeline tuned to absorb the firehose. That service updates the live Geo-index with the driver's current cell. On the read side, the rider app sends a ride request through an API gateway, the front door that also holds open the drivers' WebSocket connections. The gateway hands the request to the Matching service. The matching service queries the Geo-index for the available drivers in the rider's cell and its neighbors. It ranks them and dispatches an offer to the best one, pushing it down that driver's WebSocket through the gateway. When the driver accepts, the matching service commits the match and the Trip store runs the trip through its state machine.

1) Drivers stream location. The driver app sends updateLocation every few seconds to the Location service. The service computes which cell that latitude and longitude falls in. It updates the Geo-index so the driver is now listed under their new cell, removing them from the old one if they crossed a boundary. Nothing is durably written. The point is purely to keep the live picture of "who is where" current for the next query.

2) A rider requests, the system finds nearby drivers. The rider app calls requestRide through the gateway, which hands it to the Matching service. The matching service asks the Geo-index for the available drivers in the rider's cell and the ring of neighboring cells. That returns a small candidate set instead of the whole fleet. It ranks them, usually by ETA first. ETA is the estimated minutes for that driver to reach the pickup, which it gets from the routing engine that is out of scope to build. How that geospatial lookup is made fast and accurate is the first deep dive.

3) Dispatch, accept, and run the trip. The matching service picks the best driver. It marks that driver as offered so no other request can grab them, then pushes a dispatch offer down their WebSocket through the gateway. If the driver accepts within the window, the service commits the match atomically. The Trip store then moves the trip from requested to matched and onward through its state machine. If the driver declines or the offer times out, the lock is released and the offer falls to the next candidate. Keeping that hand-off free of double-booking under concurrency is the third deep dive.

Technology choices

Every box so far has been a category. In the room, name a real technology for each and be ready to defend it. The interviewer's favorite follow-up is the failure question: what happens when this piece dies mid-flight? Here is each pick. For each, why that kind of tool fits and how the design behaves when it breaks.

  • The API gateway is a fleet of stateless WebSocket gateway servers behind an edge load balancer (Envoy is the proxy Uber runs at its edge). Their one stateful job is holding every online driver's persistent connection, so the design keeps a connection registry mapping each driver to the gateway node that owns their socket. When a gateway node dies, its drivers reconnect to another node within seconds and the registry updates. A lost node drops connections for a moment and loses no trip.
  • The Geo-index is an in-memory, region-sharded service in the Redis family, keyed by H3 (or S2) cell so a nearby query is a lookup of one cell and its ring of neighbors. Memory is what lets it absorb the firehose, and region sharding splits the load so a London shard never touches Tokyo. Live location is disposable, so its failure mode is gentle. A downed shard is backed by a replica. Even a cold restart refills within a few seconds as every driver in that region re-reports on the next ping.
  • The Location service ingests the location firehose through Kafka, partitioned by region so each region's pings stay together and the stream splits across many brokers. Each partition is replicated across several brokers, so a broker dying is survivable: a replica takes over and producers and consumers cut to it. The consumers that fold pings into the geo-index track their offset in the log, so one that crashes resumes from its last bookmark rather than dropping updates.
  • The Trip store is a wide-column or key-value store (Cassandra or DynamoDB), partitioned by trip id (or by city) so writes spread evenly and one hot city cannot swamp a single node. A trip is written rarely and must survive a crash, and these stores give exactly that. Data is replicated across nodes, and a tunable quorum lets the match commit read and write strongly enough that driver status never goes stale. When a node fails, a replica serves the row and the trip's state machine carries on from its last recorded state.
  • The Matching service runs as stateless servers behind a load balancer. No instance holds anything a request needs. Driver status lives in the strongly-consistent store and the candidate set comes from the geo-index. Any box can handle any ride request, and a dying one just drops out of the rotation. That statelessness is what lets the matching tier scale out horizontally with request volume.

Across the options here, the brand matters less than the category and the defense behind it. Think an in-memory geospatial store in the Redis family, a partitioned log in the Kafka family, or a replicated wide-column store in the Cassandra or DynamoDB family. Naming why each category fits is what scores. A generic "Redis-style index" is a fine answer if you can defend it. The deeper signal is the same split this design rests on. The location plane is the in-memory geo-index fed by a Kafka firehose. It is hot and loose and eventually consistent. The trip plane is the wide-column trip store guarded by the matching service. It is rare and durable, and it demands strong consistency. Say clearly which tool sits on which side, and why. That is the senior read.

Potential deep dives

Most of the interview signal lives here. The interviewer picks one or two pieces and pushes until something breaks. Below are the three this round reaches for most, ordered by how central they are to the ride-matching story, each grounded in the scale the design starts from.

1) Finding nearby drivers: the geospatial index

Everything else hangs off this one. The question sounds simple: given a rider's location, return the available drivers closest to them. The naive way keeps every driver in one big table and, for each request, computes the distance from the rider to every single driver, then keeps the nearest few. Millions of drivers means millions of distance calculations for one request. The system serves many requests a second, so it collapses instantly. Every good answer reaches for the same idea: stop scanning the whole world. Partition the map into cells. Index drivers by the cell they sit in. Then a query reads only the rider's cell and the ring around it. The hard part, where the tiers split, is exactly how you cut the map into cells.

BadScan all drivers in one global table

Approach

Keep every driver as a row with a latitude and longitude. For each ride request, loop over all of them, compute the distance to the rider, and return the closest few. It is the obvious first thing to write, and it is correct.

Challenges

It does not scale even slightly. One request touches every driver in the fleet, so the work per query grows with the total number of drivers, and the system has both millions of drivers and a steady stream of queries. You can put a database index on latitude and longitude separately. That only narrows to a band on one axis and still leaves a huge scan, because a one-dimensional index cannot capture "near in 2D." The whole reason geospatial indexes exist is that ordinary indexes cannot answer "near this point on a map" efficiently.

What forces the upgrade is that a nearby query should touch a number of drivers proportional to how many are near the rider. The count should not scale with how many drivers exist on Earth. That means giving every location a key you can look up directly, so the index jumps to the rider's neighborhood instead of walking the planet.

GoodGrid buckets: geohash

Approach

Chop the map into a fixed grid of cells and give each cell a short string key, then index drivers by that key so a lookup is a hash-map read. The classic way to compute the key is a geohash. It interleaves the bits of latitude and longitude into a single string, and each added character narrows the location to a smaller box. A useful property falls out of this bit-interleaving: two places whose geohash strings share a long common prefix are usually close together, so "drivers near here" becomes "drivers whose geohash starts with this prefix." To answer a query you look up the rider's cell plus the eight neighboring cells (since a driver just over a cell boundary is still nearby) and rank only those drivers. Now one query reads a handful of cells instead of the whole fleet.

Challenges

Fixed-size cells fight uneven density. A geohash cell over quiet suburbs might hold three drivers, while a same-size cell over a downtown core at rush hour holds thousands. One prefix length is too coarse in one place and too fine in another. There is also a boundary wrinkle. Two points can sit physically next to each other in cells whose geohash strings share no prefix. So you always check the neighbor cells as well as the rider's own. Geohash is a real, workable answer, and it is a strong thing to propose. The density problem is what pushes toward the next tier.

GreatA hierarchical, density-aware index: quadtree or H3

Approach

Make the cells adapt to density instead of being one fixed size everywhere. One classic structure is a quadtree, which recursively splits any region holding too many drivers into four smaller squares. A busy downtown ends up finely subdivided while empty countryside stays coarse, and every leaf cell holds a roughly bounded number of drivers. A query walks down the tree to the rider's leaf and reads it plus its neighbors. Uber's real systems lean on cell systems built for the sphere. Google's S2 maps the globe onto a hierarchy of square cells at many resolutions, which handles the fact that the Earth is round rather than flat. Uber went further and built H3, its own open-sourced grid of hexagons. Every neighbor of a hexagon is the same distance away. A square cell has near neighbors on its edges and farther ones on its corners. That uniform neighbor distance makes "expand the search outward by one ring of cells" behave consistently in all directions, which is exactly the operation ride matching runs constantly. Both S2 and H3 are hierarchical, so you can use a coarse cell where drivers are sparse and a fine cell where they are dense.

Challenges

The hot-cell problem is the thing to name. A dense downtown cell at surge time can hold so many drivers that reading and ranking it gets expensive. A quadtree or variable-resolution H3 grid uses a finer cell there, so each cell stays bounded. The cost is more bookkeeping: a quadtree has to split and merge as drivers move, and a multi-resolution scheme has to decide which resolution applies where, which is real engineering but a solved pattern. Tell the interviewer that grid buckets get you a working nearby query. A hierarchical, density-aware index like a quadtree or H3 keeps that query fast and even when density is wildly uneven, and in ride-hailing it always is.

The Uber angle
"Use a geohash" is a fine answer that stops one step short of Uber's actual problem. Driver density is wildly uneven. The same fixed grid is too coarse downtown and too fine in the suburbs at once. So walk the chain start to finish. Scanning all drivers does not scale, so partition into cells and read only the rider's cell and its neighbors. Fixed cells fight uneven density, so use a hierarchical index that subdivides hot regions. And Uber runs H3, its own open-source hexagonal grid, where every neighbor sits at an equal distance so expanding the search ring behaves the same in all directions. Keep the two names straight: S2 is Google's spherical square-cell system, H3 is Uber's hexagonal one. Mix them up and you have read about Uber's stack without ever touching it.

2) Ingesting the location firehose

Return to the number from the start. Millions of drivers, each writing a location roughly every 4 seconds, is on the order of a million writes per second, sustained all day. The instinct to write each ping into a durable database is the trap, so meet it head on. A disk-backed database simply cannot absorb a million writes a second without an enormous, expensive cluster. And even if it could, the writes are pointless. A driver's position from four seconds ago has no value the instant a newer one arrives, since matching only ever cares about where drivers are right now. Storing that history durably would be paying a fortune to save data nobody reads.

So the live location lives in memory, in the geo-index, and each ping overwrites the previous value rather than appending to a log. Memory is fast enough to take the firehose, and overwriting keeps the footprint bounded, since the index holds one current position per driver rather than a growing history that would balloon over time. To handle the volume the index is sharded by region. The world is split into chunks, and each chunk's drivers and queries are handled by a different machine or group of machines. Sharding by region is natural here, since the data already partitions by geography. A London driver's pings and a London rider's queries both land on the London shard, and that shard never has to talk to Tokyo. This is what lets the firehose be split across many machines instead of aimed at one. It is the same partitioning that gives the system its regional fault isolation.

Two wrinkles are worth naming. First, a driver moving across a shard or cell boundary has to be removed from the old cell and added to the new one. So the location service computes the new cell on each ping and moves the driver if it changed. That is a cheap in-memory operation. Second, durable data still has a home, separate from the live index. The trip and its events go to the durable Trip store: who was matched, when the ride started and ended. If location history is ever needed for support or analytics, it is sampled and written to a separate time-series or log store at a far lower rate, not captured on every single ping. The principle is to keep the hot path completely free of durable writes: take a ping, update one in-memory cell, and let the rare, valuable events pay the cost of durability.

The Uber angle
Route a million writes a second into a relational database and you have missed the whole shape of the system. Live location is hot, disposable, eventually consistent. So it lives in memory in the geo-index, overwritten on each ping, never persisted per update. Then ground the scale-out in geography. Shard the index by region so the firehose splits across machines and a London ping never touches a Tokyo shard. The same partitioning keeps one region's outage from dragging down the rest. The durable trip log stays strictly separate. A hot, loose location plane on one side, a cold, strict trip plane on the other, and you can say which is which. That separation is the answer.

3) Matching without double-booking

Make the concurrency problem concrete. Two riders, a block apart, both request a ride at the same instant, and the geo-index returns the same nearby driver as the best candidate for both. Or one rider's request finds a driver, but before that driver accepts, a second request also offers them. If both matches go through, one driver is assigned to two trips, and at least one rider gets a car that is not coming. This is the strong-consistency requirement from the start, and it sits in the middle of a system that is otherwise happily eventually consistent. Location can be stale, but driver status cannot.

The fix is to make the match atomic and to guard driver status, even while location stays loose. When the matching service selects a driver to offer, it first locks that driver by flipping their status to offered in a strongly-consistent store. That happens in a single atomic operation that succeeds only if the driver was available a moment ago. A second request that tries to offer the same driver finds them already offered. It skips to its own next candidate, so two riders can never both hold the same driver. The lock carries a timeout, so if the offered driver does not accept within a few seconds, the lock is released automatically and the driver returns to the available pool. That prevents one slow or distracted driver from being stuck locked forever. This is why driver status, unlike location, must be strongly consistent. The lock is only meaningful if every matching attempt sees the same up-to-date status.

On top of that lock runs the dispatch offer protocol, which is sequential by design. The service offers the trip to the best candidate, pushes it down that driver's WebSocket, and waits N seconds. On an accept, it commits the match. It moves the Trip from requested to matched, confirms the driver as on-trip, and tells the rider their driver is on the way. On a decline or a timeout, it releases the lock and offers the trip to the next-best candidate, repeating that hand-off down the ranked list until someone accepts or the candidate list is exhausted. The trip itself rides its state machine the rest of the way: requested to matched to en route to in trip to completed. Each transition is an allowed, recorded step. Modeling the trip as a state machine guarantees it can never be, say, both completed and en route. It also guarantees that a crash mid-flow resumes from a known state rather than a corrupt one.

The Uber angle
One system, two consistency models, and the trap is treating it as one. A driver's pin can lag a second or two and nobody is hurt. Driver status cannot lag at all, because the cost of getting it wrong is a real double-booked driver out on the road. So guard status with a strongly-consistent atomic lock and walk the protocol. Offer the best candidate and atomically flip them to offered so no one else can take them. Wait N seconds, then fall back to the next on a decline or a timeout that auto-releases the lock. Then name the trip state machine as the thing that keeps the lifecycle correct and crash-safe. A loose location plane and a strict matching plane in one design, and you can point at which is which and why. Get that line down and the rest is detail.

Key Takeaways

  • ✓Lead with the two headline problems. Ride matching is a write firehose (millions of drivers pinging every ~4s, around a million writes per second) bolted to a latency-critical geospatial query (find the nearest available drivers in milliseconds). Organize the design around those two, and partition everything by geography.
  • ✓The geospatial index is the core. Scanning all drivers does not scale, so partition the map into cells and read only the rider's cell and its neighbors. Geohash (bit-interleaving into a grid) works, but uneven density forces a hierarchical, density-aware index. Uber uses H3, its own hexagonal grid where every neighbor is equidistant; S2 is Google's spherical square-cell system.
  • ✓Keep the firehose in memory. Do not durably write every location ping; a four-second-old position is worthless. Live location lives in the in-memory geo-index, overwritten each ping and sharded by region, while the durable trip log stays strictly separate. Hot and loose for location, rare and strict for trips.
  • ✓Match atomically without double-booking. Location is eventually consistent, but driver status is strongly consistent. Lock a driver to offered in one atomic step before dispatching, with a timeout that auto-releases. Then run the sequential offer protocol and a trip state machine (requested, matched, en route, in trip, completed) to keep the lifecycle correct.
Had your Uber interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Uber interview guide

Know what Uber asks before you walk in.

16 coding · 3 system design · 21 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.