Library
33 min read

Design Uber

Two hundred thousand drivers are moving around a city, and every one of their phones reports where it is every four seconds. A rider opens the app and wants to see the nearest cars. Thirty seconds later they want exactly one of those cars, promised to them and to nobody else.

Driver appDriver gatewayGeo indexOffer timerRider appRider APIPricingMatching serviceTrip store

Report. Every four seconds each driver app sends its position over a connection it keeps open.

Look at the two halves of that picture. They are two different systems sharing a screen:

  • The driver half (driver app, gateway, geo index) takes fifty thousand location updates a second, and it is allowed to lose all of them, because a fresh one arrives four seconds later.
  • The rider half (rider API, matching, trip store) handles about a thousand ride requests a second, and it is never allowed to lose one, because a trip is money and a legal record.

Most candidates design the driver half and hand wave the rider half. The rider half is where the interview is.

The real Uber

This page builds the core of a ride hailing app like Uber: tracking moving drivers, finding the nearest ones, and giving exactly one of them to a rider. The numbers and choices below are interview assumptions, not Uber's own. Where the page says what Uber really does, it says so by name.

Step 1: Understand the problem#

Ask about the matching before you ask about the map. Finding nearby points on a map is a solved problem with a known answer. Deciding which driver gets which rider is where the real decisions are. Each row below settles one line of the design.

You askThey sayWhat it settles
How many drivers are online, and how often do they report position?A couple of hundred thousand in a large city, every four seconds.Fifty thousand writes a second into whatever holds locations. That number alone rules out a relational table with a spatial index and pushes you to an in memory grid.
How fresh does a driver location have to be for matching?Within a few seconds.Location is soft state. It can live in memory, be lost on restart and rebuild itself in four seconds, which removes durability from the hottest write path in the system.
Can two riders be matched to the same driver?Never.A serialisation point per driver, and an offer that locks the driver while they decide. This is the hard requirement, and it is not solved by the geospatial index.
What happens if a driver ignores the request?Move to the next one, within seconds.Offers with a short timeout and automatic reassignment, so the matching state machine has a timer in it. It also means a rider is waiting while you try three drivers in sequence.
Does the price change while the rider is looking?It is quoted up front and honoured.A pricing service that reads supply and demand per area on a slow loop, and a quote that is frozen into the trip. Recomputing at accept time is how you get a support ticket.
Is a trip record allowed to be lost?Absolutely not. It is money and it is a legal record.Two different storage systems with two different guarantees. Locations are disposable, trips are durable and transactional, and confusing the two is the most common mistake in this design.

What you are building, and what you cut#

Functional requirements
What the system does.
  • Drivers report location continuously. Fifty thousand writes a second, and none of them individually matter.
  • Riders see nearby drivers and request a ride. A read of the same index the writes go into.
  • Match one rider to exactly one driver. With offers, timeouts and reassignment. This is the actual system.
  • Track the trip to completion and record it. Durable, transactional, and the source of truth for payment.
Non-functional requirements
How well it has to do it. The numbers you commit to.
  • 200,000 concurrent drivers per city, 50,000 location updates a second.
  • Nearby drivers returned in under 200ms.
  • A driver is never offered to two riders at once.
  • A trip record survives any single component failing.
Out of scope
What you cut, and say so out loud.
  • Routing and ETA computation. It is a graph problem with its own literature, and here it is a call to a maps service.
  • Payments. The trip ends with a call to a payment rail, which has its own design page: Design UPI payments.
  • Pool and shared rides. A different matching problem entirely, and adding it doubles the page.
  • Fraud, incentives and driver supply positioning. Real, enormous, and not what is being asked.

Back of the envelope#

Location write pressure
200 thousand
4 seconds
1000
Location writes per second200k / 4s = 50,000
Ride requests per second1,000
Write to match ratio50,000 / 1,000 = 50 to 1
Live location state at ~120 bytes24 MB
Location writes a day if you stored them4.3 billion
50,000 writes/sec
against a working set that fits in the memory of one laptop

Those two numbers together are the design. 24MB of live state means the whole city fits in memory, and 50,000 writes a second means you must never touch a disk to record one. The moment somebody suggests persisting every location update, ask what reads it.

Read the last two rows together. Fifty thousand writes a second sounds huge, but the whole city's live state is a few hundred megabytes, smaller than one phone photo album. That pairing is the design: everything fits in memory, and nothing on the hot path ever touches a disk.

The number people get wrong

Candidates size storage for location history and end up designing a time series database. Nothing in the matching path ever reads a location older than a few seconds. Keep the live position in memory, and if the business wants trip traces for analytics, publish them to a log the matching system never queries (the difference between a queue and a log is in queue vs pub/sub vs event stream). Two systems, two lifetimes.

Step 2: Propose the high level design#

The API#

Four calls. The first runs fifty thousand times a second, so it is as small as an API call can be. The last one is where two drivers can collide.

POST/v1/drivers/location
{
  "driverId": 88213,
  "lat": 12.9352,
  "lng": 77.6245,
  "heading": 138,
  "at": "2026-09-10T09:14:22Z"
}
returns 204
Why: No response body, no acknowledgement of durability, and it should be a persistent connection rather than a fresh request every four seconds. This is the highest volume call in the system and every byte in the response is multiplied by fifty thousand a second.
GET/v1/nearby?lat=12.93&lng=77.62&radius=2000
returns 200 { drivers: [{ id, lat, lng, etaSeconds }], quoteId: "q_71a2" }
Why: The quote id comes back with the list. Freezing the price here rather than at accept time is a product decision that removes an entire class of complaint, and it means the pricing read happens once per search rather than once per accept.
POST/v1/trips
{
  "riderId": 4471,
  "quoteId": "q_71a2",
  "pickup": {...},
  "dropoff": {...}
}
returns 202 { "tripId": "t_9f31", "state": "matching" }
Why: 202, because no driver has agreed yet. Returning 200 with a driver would mean holding the request open while you offer the ride to three people in sequence, and the client would time out before the matching did.
POST/v1/offers/{id}/accept
returns 200 { tripId, rider, pickup } or 409 if it expired
Why: The 409 is the important response. Two drivers can tap accept on offers that were valid when they were sent, and exactly one of them can win. The loser needs a clean answer, not a trip they cannot see.

The data model#

Two stores, and the split between them is the design. The first holds where drivers are right now. The second holds trips. A geohash (used as the key below) is a short string that names a square on the map: tdr1vx is one square about 1.2 km wide, and points close together usually share the same string. Design a proximity service shows how a search covers a circle with these squares.

driver_locationin memory, keyed by geohash cell
cellgeohash(6)PKAbout 1.2km on a side. A search reads this cell plus its eight neighbours, never a range scan.
driver_idbigintPKMember of the cell set. Moving cells is a remove and an add.
lat, lng, headingfloatOverwritten every four seconds. There is no history and nothing reads the previous value.
stateenumIDXavailable, offered, on_trip. Only available drivers are returned, and this field is why a search is cheap.
updated_attimestampA driver whose entry is older than 30 seconds is treated as offline. Expiry, not a delete path.
Sample row
tdr1vx | 88213 | 12.9352, 77.6245, 138 | available | 09:14:22
Nothing here is durable and that is deliberate. Lose the whole store and it rebuilds in four seconds from the next round of updates, which is a far better property than any replication scheme would give you.
tripsrelational, sharded by city, replicated
trip_iduuidPKGenerated at request time, before any driver exists.
rider_idbigintIDX
driver_idbigintIDXNull until an offer is accepted. The unique constraint on active trips per driver is the last line of defence against double assignment.
stateenummatching, assigned, arriving, in_progress, completed, cancelled. Transitions are guarded, not free.
quote_idvarchar(32)The price the rider was shown. Copied into the trip so it cannot drift.
versionintOptimistic lock. Every state change is a compare and set, which is how two concurrent accepts resolve.
Sample row
t_9f31 | 4471 | 88213 | in_progress | q_71a2 | 7
Trips are the money and the legal record, so they get a real database with real transactions. Putting trips in the same store as locations, to save a system, is the mistake this table exists to prevent.

The version column is an optimistic lock: every change says "update this row only if the version is still 7", so two writers racing on one row cannot both win. The optimistic vs pessimistic locking article shows the race step by step.

The whole system on one whiteboard#

Figure 1 is the picture at the top of the page, with maps and payments added.

Figure 1. The top half takes 50,000 writes a second and is allowed to lose everything. The bottom half takes a thousand requests a second and is not allowed to lose anything. Almost every decision here follows from that split.

Walking Figure 1:

  1. Driver apps keep one connection open and stream their position through it. Opening a fresh HTTPS request every four seconds for 200,000 drivers is a denial of service attack you run on yourself. Which kind of long lived connection to use is the subject of WebSockets vs SSE vs long polling.
  2. The gateway overwrites the driver's entry in the geo index. No append, no history, no disk. If the driver crossed into a new square, that is a remove from the old one and an add to the new one.
  3. A rider opens the app and asks who is nearby.
  4. That search reads the same in memory index the drivers write into, keeping only drivers who are available.
  5. The price is quoted once, here, and frozen into a quote id. It cannot drift between looking and accepting.
  6. Requesting a ride hands the problem to the matching service, which answers at once with a trip in the matching state. No driver has agreed yet, which is why the API returns 202.
  7. Matching reads candidate drivers from the index and ranks them by ETA (estimated time of arrival), not by straight line distance, because a river or a one way street makes those two numbers very different.
  8. The trip row is created before any driver is contacted. If everything crashes now, there is a durable record saying a rider is waiting.
  9. An offer goes to one driver with a short expiry, and a timer owns what happens if they do not answer. That timer is the part people forget, and it is the difference between a system and a demo.
Your answer

Step 9 offers the ride to one driver at a time with a 15 second timer. The alternative is broadcasting to the nearest five and letting them race. Which would you pick, and what does the other one cost?

Step 3: Design deep dive#

Finding nearby drivers without a spatial database#

Picture a city map with a grid drawn over it. To find cars near you, you do not measure the distance to every car in the city. You look in your own square and the squares touching it. That is the whole trick, and the ladder below is how you arrive at it.

Where do driver positions live so that a nearby search is fast?

  1. First try
    A database table with a spatial index
    Store each driver as a row with a point column, add a spatial index (an R-tree, a structure that keeps nearby points in the same branch), and search with a bounding box: every driver whose point falls inside this rectangle.
    Where it breaks: Every location update is a write that rebalances the R-tree. At fifty thousand updates a second the index spends its life reorganising itself, and the database also writes each update to disk for durability nobody needs.
  2. Better
    Squares on the map, kept in memory
    Give every driver the geohash of the square they are in. Keep a set of driver ids per square, in memory. An update is a remove from one set and an add to another. A search reads your square and its eight neighbours, nine set reads in total.
    Where it breaks: The square size is fixed. In a dense market nine squares can return two thousand drivers, and in a rural one they return nobody. Squares also stretch away from the equator, so the same precision covers different areas in different cities.
  3. What you ship
    Squares to filter, ETA to rank
    Keep the squares for the cheap first cut, take the twenty closest by straight line, and ask the maps service for real ETAs on only those twenty. Start with geohash, and move to S2 or H3 cells, which are nearly equal in area everywhere, when the distortion measurably costs you matches.

The three ways of cutting up the map, side by side:

Nearby lookup
Encode the point to a fixed precision string, then read that cell and its eight neighbours. Nine set reads from memory, no index to maintain, and a write is a set remove plus a set add. The cost is that cell size is fixed: in a dense market nine cells returns two thousand drivers, and in a rural one it returns nobody.

In the interview, start with geohash cells and say why: it is a hash map of sets, a write is two operations, and nothing needs rebalancing under load. Then name its weakness before the interviewer does, which is that a fixed cell size is wrong somewhere in every city, and say that S2 is what you move to when that starts costing you matches.

What actually gets returned

Sorting candidates by straight line distance is the beginner version and it produces visibly stupid matches: the closest car is across a river with a ten minute detour to the bridge. Rank by ETA from the maps service, on the handful of candidates the cells gave you. Nine cells might return two hundred drivers, you take the twenty closest, and you ask for real ETAs only for those twenty. That is the shape of nearly every geospatial ranking problem: cheap filter, expensive rank, small candidate set.

One driver, one rider, no exceptions#

This is the part that separates the answers. Two riders, two matching workers, one driver nearest to both, and nothing in the geo index stops both workers from picking that driver.

It is the cinema seat problem. Two people look at the seat map, both see seat 14C free, both tap it. The seat map was correct when each of them looked. What matters is who gets to change it.

How do you make sure a driver is promised to only one rider?

  1. First try
    Check, then assign
    The matcher reads the driver state, sees available, and writes offered.
    Where it breaks: Matcher A reads available. Matcher B reads available a millisecond later. Both write offered, both send an offer, and one driver now has two riders. This is called a lost update: the second write silently overwrites the first.
  2. Better
    Lock the driver for the whole trip
    Take a distributed lock on the driver before offering, and hold it until the trip ends.
    Where it breaks: Far too coarse. The lock is held for forty minutes to protect a decision that takes milliseconds, and a matcher that crashes while holding it strands the driver until someone notices.
  3. What you ship
    Compare and set, with a lease that expires
    Change the driver state with one atomic step: "set it to offered, but only if it is still available". Exactly one matcher succeeds. The loser is not an error, it simply takes its next candidate. The offered state carries a 15 second lease, so it expires on its own if nobody follows up.

Figure 2 plays the race with the third rung in place. A compare and set (CAS) is that atomic "change it only if it still equals what I saw" step.

1/6 Matcher A is handling rider 4471 and gets a list of nearby drivers. Driver 88213 is at the top, ranked by ETA.
Figure 2. The offer is a lock with a timer on it. The second matcher does not fail because the driver is busy, it fails because a compare and set on the driver's state did not succeed.

Two details worth saying out loud. The lease has to expire on its own, so a matcher that crashes between the compare and set and the offer does not leave a driver stuck as offered for ever. And the accept has to be idempotent, meaning doing it twice has the same effect as doing it once, because a driver on a patchy connection will tap accept twice and both taps will arrive. Idempotency keys shows how that is built.

The follow up you will get

"Why not broadcast to the five nearest drivers and let the fastest one win?" It matches faster and riders like it. It also means four drivers get a notification for a ride they cannot have, which is the single most complained about behaviour in every driver app that has tried it, and it turns acceptance into a race decided by phone hardware and network latency. The usual compromise is sequential offers with a short timeout, and broadcast only when sequential has already failed twice.

Sharding a city, and what happens at the edges#

Sharding means splitting the work so each machine owns one slice of it (the sharding chapter starts from the beginning). For matching, the natural slice is a piece of the map: the matcher that owns a region is the only one that changes the state of drivers inside it, which keeps the compare and set cheap and local.

How do you split matching across machines?

  1. First try
    One matching service for the whole country
    Every request in the country goes to one matcher, so there is only ever one place that changes driver state.
    Where it breaks: One process cannot keep up with a country, and when it restarts, nobody anywhere gets a ride.
  2. Better
    One matcher per grid region
    Cut the map into regions and give each to one matcher.
    Where it breaks: The boundary. A rider standing on the line between two regions has candidate drivers owned by two different matchers, and if both matchers assign, you are back to double booking.
  3. What you ship
    Shard by city, and route boundary drivers through their owner
    Cities are natural boundaries, since almost nobody takes a ride from one city into another. Where a boundary does matter, the matcher for the rider owns the trip. It may read candidates from a neighbouring region, but it must acquire each driver through the matcher that owns that driver. Cross region matches are a small share of traffic and are allowed to be slower.

Break it#

City load
tuesday 3pm
tuesday 3pmfriday 6pmstadium emptiesgateway restartrepaired
Healthy. 200,000 drivers, a thousand requests a second, most drivers idle. The geo index is doing 50,000 memory writes a second, which is nothing, and matching is mostly waiting.
Read every step as text
  1. tuesday 3pm · Healthy. 200,000 drivers, a thousand requests a second, most drivers idle. The geo index is doing 50,000 memory writes a second, which is nothing, and matching is mostly waiting.
  2. friday 6pm · Degraded. Demand triples and supply does not. Matching is now working hard, but the bottleneck is not CPU: it is that there are no available drivers in the cells near most riders, so candidate lists come back short and matchers walk further out.
  3. stadium empties · Down. Twenty thousand people request a ride from four adjacent cells inside two minutes. Those cells are one hot partition of the geo index and one matcher owns the region, so a citywide system is bottlenecked on one process while the rest idle. This is a hot key problem wearing a map.
  4. gateway restart · Degraded. A gateway deploy drops 60,000 driver connections. They reconnect over the next few seconds, and the geo index has stale entries for all of them until they do. Because entries expire after 30 seconds rather than being deleted, the worst case is briefly offering rides to drivers who are reconnecting.
  5. repaired · Healthy. For the surge: split the hot region into smaller matcher shards dynamically, and queue riders with a visible wait rather than failing them. For the restart: stagger the deploy so no more than a few percent of connections drop at once, which is a deployment policy rather than an architecture change.

The stadium case is the one to talk about unprompted. Every geospatial system has it, the map makes it look like a new problem, and it is the same hot partition (one slice getting far more traffic than the rest) you would get from a celebrity account or a viral link. The sharding chapter has a Break it section on exactly this.

Trade-offs#

ChoiceWhat you gainWhat you payPick it when
Locations in memory, trips in a databaseThe 50,000 writes a second never touch a disk, and the records that matter get real transactions.Two systems to operate, and a rule everyone has to remember about which data goes where.Always. The single store version either cannot take the write rate or is not durable enough for the money.
Geohash cells over a spatial indexA write is two set operations and a read is nine memory lookups, with nothing to rebalance.Fixed cell size is wrong in both dense and empty areas, and cells distort away from the equator.From the start. Move to S2 when you can measure the distortion costing you matches.
Sequential offers with a timeoutOne driver is asked at a time, so nobody gets a notification for a ride they cannot have.Slower matching when drivers are slow to answer, since three offers in sequence can take 45 seconds.Default. Fall back to a broadcast round only after sequential offers have already failed.
One matcher per regionAssignment is a local compare and set with no distributed coordination.Boundaries need special handling, and a hot region bottlenecks on one process.Any system where the natural unit of contention is geographic, which is all of them.

Interview replay#

Interviewer
A rider opens the app. Walk me through finding nearby drivers.
Warm up, and a check on whether you reach for PostGIS.
You
Driver positions live in memory, bucketed by geohash cell. The rider point encodes to a cell, we read that cell plus its eight neighbours, filter to available drivers, take the twenty closest by straight line, and only then ask the maps service for real ETAs on those twenty. Cheap filter, expensive rank, small candidate set. I would avoid a spatial index in a database here because we are doing fifty thousand position writes a second and that is not what R-trees are for.
Gives the shape and the reason against the obvious alternative, with the write number as the justification.
Interviewer
Two riders request at the same moment and the same driver is nearest to both.
The actual question.
You
Both matchers will see him, and that is fine. The assignment is a compare and set on the driver state from available to offered with a short lease. One wins and sends the offer, the other gets a failed CAS and moves to its next candidate. The lease has to expire on its own so a matcher crashing does not strand the driver, and the trip row carries a version so the accept is also a compare and set.
Does not try to prevent the race, resolves it. That is the senior answer.
Interviewer
Why not just broadcast to five drivers?
Testing whether you can argue a product trade, not only a technical one.
You
It matches faster and riders prefer it. The cost is that four drivers get buzzed for a ride they will not get, which drivers hate, and acceptance becomes a race decided by whose phone is faster. I would do sequential offers with a 15 second timeout by default and only broadcast after two failed offers, when the rider has already been waiting thirty seconds and the calculus changes.
Answers with a policy rather than a preference, including when the policy flips.
Interviewer
A concert ends and twenty thousand people request a ride from the same block.
The failure question. They want to see if you recognise the shape.
You
That is a hot partition. Those requests all land in a handful of cells owned by one matcher, so one process is saturated while the rest of the city idles. Adding matchers does not help unless you split that region, so I would support dynamic subdivision of a hot region into smaller shards. I would also queue riders with an honest visible wait rather than failing them, because supply is genuinely finite at that moment and a queue is a better experience than a retry loop.
Names it as a hot partition, rejects the non fix, and includes the product answer alongside the technical one.
Interviewer
How long should the offer timeout be?
Open ended, checking for a confident invention.
You
I do not know, and I would not guess it in the room. It is a trade between rider wait time and driver acceptance rate, and the right way to find it is to measure the distribution of how long drivers actually take to respond and cut somewhere in the tail, then A/B it. My instinct is somewhere between ten and twenty seconds, and I would expect it to differ by market and by time of day.
Gives a method and a range, then admits it is an instinct. Inventing "12 seconds because that is optimal" is the answer that loses points.

Checkpoint#

Checkpoint

1. Why keep driver locations in memory rather than in a replicated database?

2. Two matchers pick the same driver from the index at the same instant. What prevents a double assignment?

3. Same system, but now it delivers food instead of people. What changes first?

Worth memorising
  • 200,000 drivers reporting every 4 seconds is 50,000 writes a second.
  • The entire live location state is a few hundred megabytes. It fits in memory and it is disposable.
  • Nine cells cover a search: your own, plus the eight neighbours.
  • One driver, one rider, settled by a compare and set with an expiring lease. The loser just takes the next candidate.
Say this in 60 seconds

A ride hailing service is two systems that share a screen. Driver locations are 50,000 writes a second into an in memory index bucketed by geohash cell, with no durability at all, because nothing reads a position older than a few seconds and the whole state rebuilds itself in one reporting interval. Trips are the opposite: a real database with transactions, because that is money and a legal record. A nearby search reads nine cells, filters to available drivers, and ranks the top twenty by real ETA rather than straight line distance. Matching is the hard half. Two matchers will see the same driver, so I resolve it with a compare and set on the driver state from available to offered with an expiring lease, and the loser moves to its next candidate. Offers go to one driver at a time with a short timeout and automatic reassignment. The failure mode I would call out is a concert letting out, which is a hot partition in a handful of cells owned by one matcher, and the fix is splitting that region dynamically plus an honest queue for riders rather than a retry loop.