The system design question
Design a ride-sharing service like Uber. Drivers send their location, riders request a ride, and you need to match them quickly.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Drivers sending their location | Pricing and surge |
| Finding drivers near a rider | Payments |
| Offering the ride and matching | Maps and turn-by-turn routes |
| Tracking the trip | Ratings and driver onboarding |
| Where trip data lives |
Then agree on what "good" means:
- Quick match: a rider sees a driver accepted within a few seconds.
- One driver, one ride: a driver never gets two riders at once, and a rider never gets two drivers.
- Fresh locations: the driver positions we search are only a few seconds old.
- Nothing lost: once a trip starts, its record is safe even if a server dies.
Rough numbers (5 minutes)
Made up, but reasonable: 1 million drivers online at the busiest time, each sending a location every 4 seconds. About 10 million rides a day, with the evening peak at roughly 3 times the average.
- ~1M
- drivers online at a peak
- ~250K
- location updates per second
- ~10M
- rides a day
- ~350
- ride requests per second at a peak
| Question | Rough math | What it means |
|---|---|---|
| How many location updates? | 1M drivers ÷ one every 4 seconds | About 250K writes a second. Too many for a normal database |
| How much memory for live locations? | 1M drivers × about 100 bytes | About 100 MB. Tiny. It all fits in memory |
| How many ride requests? | 10M ÷ 86,400 seconds, × 3 at a peak | About 115 a second on average, maybe 350 at a peak. Small |
The overall design (10 minutes)
- Driver apps send their location every few seconds to a location service. It updates an in-memory index that maps each small map cell to the drivers in it.
- A rider asks for a ride. The matching service looks up the rider's cell and the cells around it, gets a short list of free drivers, and ranks them by how long they'd take to arrive.
- It offers the ride to the best driver. If they accept within a few seconds, the trip is created. If not, it moves to the next one.
- Once matched, the driver's location updates also go to the rider's app over an open connection, so they can watch the car arrive.
- Trips are written to a database. Location history goes to a stream for later use, like working out what the driver gets paid for the distance.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Send location | Driver app | Driver ID, latitude, longitude, heading, and whether they're free |
| Request a ride | Rider app | Pickup and drop-off points, ride type, and a request ID so retries don't book twice |
| Accept or decline an offer | Driver app | Offer ID |
| Trip updates | Server, to the rider app | Driver location, arrival estimate, trip status |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Live driver locations | In memory, map cell to drivers, split by city | Updated 250K times a second and only the latest matters |
| Driver status | Same in-memory store, keyed by driver | Free, offered, or on a trip. Changed with a check-and-set so only one ride wins |
| Trips | Relational database, split by city | Few writes, needs to be correct, and has clear rules like one active trip per driver |
| Location history | A stream, then cheap storage | Used for fares, disputes and improving arrival estimates |
| Riders and drivers | Relational database | Small, rarely changes |
Step 1: Tracking drivers and finding nearby ones (12 minutes)
You can't ask a normal database 'who's within 2 km of this point' 350 times a second while it's also taking 250K updates a second. Instead, cut the map into cells, using something like geohashes or hexagons. That's geospatial indexing: every point belongs to exactly one cell, and nearby points usually share a cell or sit in neighboring ones.
Keep a map from cell to the set of drivers in it. When a driver's update puts them in a new cell, take them out of the old one and add them to the new one. Most updates stay in the same cell and only change the stored position.
To find drivers near a rider, look in the rider's cell and the ring of cells around it. If there aren't enough free drivers, widen to the next ring. Then rank the short list by real driving time, not straight-line distance, since a driver across a river can be close on the map and 20 minutes away.
Split the index by city or region, which is sharding by place. Rides almost never cross cities, so each shard handles its own drivers and riders. A big city might need several shards, split by area.
Picking a cell size
Too big and every search returns thousands of drivers. Too small and you need lots of rings to find anyone in quiet areas. Something around a few hundred meters wide works for cities, and the widening rings handle the suburbs.
Step 2: Matching without double booking (10 minutes)
Two riders near each other can ask at the same moment, and both see the same free driver at the top of their list. If you offer both rides to that driver, someone gets let down.
So before offering, flip the driver's status from 'free' to 'offered' with a check-and-set: it only works if the status is still 'free'. One request wins. The other sees it failed and moves to its next driver. The offer has a timer, around 10 seconds. If the driver doesn't answer, flip them back to free and move on.
Offer to one driver at a time. Offering to several at once is faster, but then several drivers accept and all but one get told 'never mind', which drivers hate.
Riders tap the button twice and phones retry on bad networks. The request carries an ID made on the phone, and the server ignores a second request with the same ID. That's idempotency, and it stops two cars showing up for one person.
Step 3: Where the data lives, and what happens when things split (8 minutes)
Locations and trips want different things. Locations are a flood of tiny writes where only the latest one matters, so a lost update costs nothing. Trips are few, and they have to be right: one active trip per driver, a clear status, a fare. That's why locations sit in memory and trips go in a relational database. This is the SQL vs NoSQL question interviewers push on, and the honest answer is 'both, for different data'.
Then the CAP question: if part of the network splits, do you keep working or stop? For locations, keep working. A slightly old position is fine. For matching a driver to a trip, choose correct over available. It's better to make a rider wait a few seconds than to put one driver on two trips.
Keeping each city's matching on one shard helps here, since the check-and-set happens in one place and doesn't need machines in different places to agree.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Live locations in memory | Writing every update to a database | 250K writes a second of data that's stale in 4 seconds isn't worth storing safely | We needed exact location history for every second, which the stream already gives us |
| Map cells | Searching by distance on every request | Looking up a few cells is cheap and simple | We needed very precise shapes, like airport pickup zones, which can be added as their own lookup |
| Offering to one driver at a time | Offering to several at once | No wasted accepts, and drivers trust the offers | Matching got too slow in quiet areas, then offer to two with a quick follow-up |
| Correct over available for matching | Always answering, even during a split | Double-booked drivers are worse than a short wait | Never, for matching |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| A location server dies | Its drivers send their next update in 4 seconds to a new server, which rebuilds the index from those updates |
| A driver loses signal | Their position goes stale. Drop drivers with no update in 30 seconds from search, and keep the trip going |
| A driver accepts but the confirmation is lost | The driver app retries with the same offer ID, and the server returns the same answer |
| A concert ends and 10,000 people ask at once | Matching in that area runs short of drivers. Widen the search, queue requests, and let pricing handle the rest |
| The trip database is slow | Matching holds the driver in 'offered' a bit longer. Locations keep flowing since they don't touch it |
Follow-up questions (5 minutes)
How would you add shared rides?
Match a new rider to a car already on a trip if the detour is small. It's the same nearby search, but ranking now cares about the route, not only the pickup.
How do you estimate arrival time?
Use road travel times between cells, adjusted by recent real trips at that time of day.
What if a city is too big for one shard?
Split it by area. Searches near a border check the neighbor shard's cells too.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–32 | Tracking drivers and finding nearby ones |
| 32–42 | Matching without double booking |
| 42–50 | Where the data lives, and what happens when things split |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Geospatial indexingFind what's nearby with map grids like geohashes and H3.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Consistency and the CAP theoremWhat happens when copies of your data can't agree.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Real-time deliveryPush updates to users the moment they happen, with WebSockets and friends.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Bloomberg.