The system design question
Design the backend that shows a customer the restaurants near them when they open Uber Eats. Focus on how you store restaurant locations and query them fast. What happens in a dense city center, or where there's nothing nearby?
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Storing restaurants and where they are | Ranking and personalization, beyond a simple sort |
| Finding the ones near a customer, fast | Menus and ordering |
| Packed city centers and empty areas | Delivery partners |
| Restaurants opening, closing and changing hours | Payments |
Then agree on what "good" means:
- Fast: the home screen waits on this, so aim for under 100 ms on the server.
- Correct enough: don't show a place that's closed or can't deliver to you. A place that opened two minutes ago showing up a bit late is fine.
- Works everywhere: a downtown block with 500 restaurants and a small town with 3 both get a sensible list.
- Lots of reads: people open the app far more often than restaurants change.
Rough numbers (5 minutes)
Assume 1 million restaurants worldwide, 100 million app opens a day, and a peak around dinner about five times the average.
- ~6K
- searches per second at dinner
- ~1 GB
- to hold every restaurant's search fields
- ~1M
- restaurants in total
- <100 ms
- target for one search
| Question | Rough math | What it means |
|---|---|---|
| How many searches? | 100 million ÷ 86,400 × 5 for the peak | About 6,000 a second at dinner |
| How big is the search data? | 1 million × about 1 KB | About a gigabyte. It fits in memory on every search server |
| How often do restaurants change? | Say each changes a few times a day | A few million small updates a day, tiny next to the reads |
| How many per area? | A busy downtown cell might have a few hundred | We'll have to cut those down before sending them |
The overall design (10 minutes)
- The app sends the customer's location and asks for nearby restaurants.
- The search service works out which map cells cover the area around them, and reads the restaurants listed in those cells from an in-memory index.
- It drops anything closed or too far to deliver, sorts what's left, and returns the first page.
- The app then fetches menus and photos for what it shows, mostly from a cache.
- When a restaurant changes in the main database, a change event updates the index within a few seconds.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Nearby restaurants | Customer app | Latitude, longitude, optional filters like cuisine, and a page cursor |
| Restaurant details | Customer app | Restaurant ID, and we return the menu, photos and hours |
| Update a restaurant | Restaurant tools | Restaurant ID and the new location, hours, delivery radius or open status |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Restaurants: name, address, coordinates, hours, delivery radius | A relational database | The source of truth. It's small and has clear relations |
| Cell ID to restaurants in it | In memory on every search server | The fast lookup for 'who's near this point' |
| Open or paused right now | In the index, updated from the change stream | Restaurants pause when they're busy, so it changes often |
| Menus and photos | The database, with a cache in front | Read a lot, changed rarely |
Step 1: Storing locations so you can search them (10 minutes)
A normal index on latitude and longitude can't answer 'what's within 3 km'. It can narrow one column, but then it has to scan everything in that band. So turn a location into something one index handles well: a cell ID. That's geospatial indexing.
Cut the map into cells, like hexagons or a geohash grid, about 1 km across. Store each restaurant's cell ID next to its coordinates. Now 'near this point' means: find the customer's cell and its neighbors, grab everyone in those cells, and check the exact distance on that short list.
The schema stays simple. The restaurant table has coordinates, cell ID, hours, delivery radius and status, with an index on cell ID. For speed, the search servers keep a map from cell ID to a list of restaurants in memory, built from that table.
- Delivery radius: a restaurant 4 km away with a 3 km radius shouldn't show up. Check each one's radius against the real distance.
- Why not a geo database? You could, and saying so is fine. Explaining cells shows you know what it's doing underneath.
Step 2: Packed city centers and empty areas (10 minutes)
Fixed cells break at both ends. Downtown, one cell can hold 500 restaurants, so we'd read and sort a lot for every search. Out in the country, the customer's cell and its neighbors might be empty, even though there's a good place 8 km away.
For dense areas, use smaller cells, or stop once you've got enough. Search the customer's own cell first, then the ring around it, and keep going outward until you have, say, 200 candidates or you hit the max delivery distance. Downtown stops after one ring. A small town keeps going.
For empty areas, that same outward search just runs out of rings. If nothing's in range, return an empty list with a reason, so the app can say 'nobody delivers here yet' instead of a blank screen. Maybe show pickup options farther away.
- Uneven load: a few downtown cells get most of the searches. Since every search server has the whole index, any of them can answer, so hot cells don't pile onto one machine.
Step 3: Keeping it fast and fresh (10 minutes)
Most of the time goes into reading candidates, filtering and sorting. Keeping the index in memory on each search server removes the network hop for the location part. With about a gigabyte of data, every server can hold all of it.
You could also cache whole results per cell, since everyone in the same small cell sees nearly the same list. Keep that cache for a minute at most, because open and paused changes fast at dinner time.
Freshness comes from the change stream. When a restaurant edits its hours or pauses orders, the database records it and an event goes to every search server, which updates its copy. A few seconds late is fine. If a server restarts, it loads a snapshot and then catches up on recent changes.
Where SQL fits
The restaurant records live in a relational database. They're small, have clear fields, and the restaurant tools need normal updates. The read path doesn't hit it at all, which is why SQL or NoSQL isn't the big question here. The in-memory index is.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Cells plus an exact distance check | Scanning by latitude and longitude ranges | One cheap lookup gets a short list to check | We needed shapes like real delivery zones, then store zones per cell |
| The whole index on every search server | Split the index across machines | It's only a gigabyte, and any server can answer any search | Data grew way past one machine's memory |
| Searching outward in rings | One fixed radius for everyone | Dense areas stop early and empty areas keep looking | Product wanted a strict 'within X km' list |
| A short result cache per cell | No cache | Takes a lot of repeat work off at dinner time | Results got personal enough that nobody shares them |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| A search server dies | The load balancer sends traffic to the others, since they all have the full index |
| The change stream falls behind | Paused restaurants show up for a bit. Alert on lag, and recheck status when someone opens a restaurant |
| The restaurant database is down | Searches keep working from memory. Only restaurant edits fail |
| A restaurant has the wrong coordinates | It shows up in the wrong place. Check addresses against coordinates when they're saved |
| Dinner rush in one city | Add search servers. They're all the same, so it's easy |
| A server restarts with an old snapshot | It catches up from the stream before taking traffic |
Follow-up questions (5 minutes)
How would you rank them better than by distance?
Get a few hundred candidates from the location index, then score them on distance, rating, delivery time and what this customer usually orders. The location index only has to find candidates.
What if the customer moves the pin to a different address?
It's the same search with a different point. The app just sends the new coordinates.
How would you show an estimated delivery time for each one?
Use a rough estimate from distance and how busy the restaurant is, saved per cell. Work out a real one only when they open the restaurant.
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–30 | Storing locations so you can search them |
| 30–40 | Packed city centers and empty areas |
| 40–50 | Keeping it fast and fresh |
| 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.
- Caching strategiesMake reads faster, and figure out what to do when data changes.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Search and inverted indexesFind text fast, rank what comes back, and keep the index fresh.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Uber.