Design Maps and Navigation
Design a maps app end to end: map tiles on a CDN, fast routes on a huge road graph, live traffic from phone pings, ETAs and rerouting.
On this page
You open a maps app, the city appears in a blink, you type an address, and a blue line with "10 min" shows up. You start driving. A few minutes later the app says "Faster route found, saves 5 min" because a crash has just blocked the motorway ahead.
Interviewers like it because three systems hide behind one screen: static content (the map), an algorithm (shortest paths on a huge graph) and a stream (moving phones turned into traffic). A strong answer keeps them apart and goes deep on one. The steps follow A Framework for Any Design. Try each "Your turn" box before you read on.
Clarify the problem
Design a maps and navigation service.
Three features, I think: show the map, compute a route with an arrival time, and guide the driver turn by turn with live traffic. Is search for places in scope, and do we need walking and transit?
Driving only. Assume address search exists and gives you coordinates.
What scale? Daily users, routes a day, and how many people navigate at the busiest moment?
About 100 million daily users, 100 million route requests a day including reroutes, and 5 million phones navigating at peak.
Then the map is static content for a CDN, routing must be fast on a continent-sized graph, and traffic is a stream of about a million pings a second. I'll aim for routes under 200 ms and new jams visible within 2 to 3 minutes. If traffic fails, routing keeps working on typical speeds.
- Functional: show a pannable, zoomable map; compute the fastest driving route between two points with an ETA; while driving, update the ETA from live traffic and offer a faster route when one appears; reroute when the driver leaves the route.
- Non-functional: map tiles load in under 100 ms from nearby; a route in under 200 ms at the server; a new jam shows up in ETAs within 2 to 3 minutes; routing stays available without live traffic; location data is anonymised and kept briefly.
- Out of scope: place search, walking and transit, map editing tools, street-level imagery.
Estimate the scale
A day has 86,400 seconds.
- Tiles: if each user loads about 100 map tiles a day, that is 100 million × 100 = 10 billion tiles a day, or 1010 ÷ 86,400 ≈ 116,000 a second, about 350,000 at a 3x peak. If the CDN answers 95% of them, the origin sees 5% of 350,000 ≈ 17,000 a second.
- Routes: 100 million ÷ 86,400 ≈ 1,200 a second on average, about 3,500 at peak.
- Pings: 1 million batches a second at peak (worked below). At about 100 bytes each that is 100 MB/s: a stream, not a database.
- Road graph: assume 500 million junctions and 1.2 billion one-way road segments. At 8 bytes a junction (two packed coordinates) and 12 bytes a segment (target, travel time, segment id), that is 4 GB + 14.4 GB ≈ 18 GB. The whole graph fits in the memory of one large server.
The world is cut into tiles: 1 at zoom 0, 4 at zoom 1, and 4z at zoom z. You store vector tiles for zoom 0 to 14 and the phone draws closer zooms itself. How much storage is that?
- 4z tiles at zoom z, for z = 0 to 14
- About 10 KB per tile on average (most ocean tiles are nearly empty)
Show the working
- Zoom 14 alone: 414 ≈ 268 million tiles
- All levels: 1 + 4 + … + 414 = (415 − 1) ÷ 3 ≈ 358 million tiles (each level adds a third more)
- 358 × 106 × 10 KB ≈ 3.6 TB
Estimate 3.6 TB (anything from 1 TB to 15 TB is a good answer)
Pre-rendered images down to street level (zoom 18) would be (419 − 1) ÷ 3 ≈ 92 billion tiles; at 20 KB each that is about 1.8 PB. Stopping at zoom 14 and drawing on the phone is what makes the whole map small enough to rebuild often.
At peak, 5 million phones are navigating and each sends one batch of GPS points every 5 seconds. How many writes per second reach the servers?
- 5 million phones navigating at peak
- One batch every 5 seconds per phone
Show the working
- 5,000,000 ÷ 5 s = 1,000,000 batches a second
Estimate 1 million/s (anything from 300 thousand/s to 3 million/s is a good answer)
A million writes a second that are only useful for a few minutes belong in a partitioned stream, aggregated in memory, never written row by row to a database.
Write the calls for loading the map, asking for a route, and sending location while driving. How does the phone learn that a faster route exists?
Compare with a model answer
Tiles are plain cacheable GETs by zoom, x and y. A route is a POST with origin, destination and departure time. Pings are a POST with a batch of points. The interesting part is the last question: the phone already talks to the server every 5 seconds, so the reply to a ping can carry "a better route is ready".
Define the API
GET /tiles/v812/{z}/{x}/{y}.mvt served by the CDN, cached for days
200 vector tile (roads, labels, areas)
404 outside the map
POST /routes
body: { "from": [52.37, 4.89], "to": [52.09, 5.12], "depart": "now" }
200 { "route_id": "r_51x", "polyline": "...", "eta_s": 600,
"steps": [ { "turn": "left", "road": "...", "at_m": 1200 }, ... ],
"graph_version": 812 }
404 no road connects the two points
503 routing overloaded; the app retries with backoff
POST /nav/{route_id}/pings
body: { "points": [ [t, lat, lng, speed, heading], ... ] } 5 points, one a second
200 { "eta_s": 540, "better_route": null }
200 { "eta_s": 1020, "better_route": { "route_id": "r_52k", "saves_s": 300 } }The decision worth arguing is how the phone hears about a faster route. A WebSocket per driver means 5 million open connections. The phone already pings every 5 seconds, so the reply carries the ETA and any better route: no extra connection, at most 5 seconds late. See realtime communication for when push is worth it.
The tile URL contains the map version (v812). A rebuild changes the URLs, so the CDN never needs purging and a phone never mixes two builds. The cost: a new version starts with a cold CDN, and the origin would see the full 350,000 requests a second. Push the popular tiles to the CDN before the app switches versions.
List what you store: map tiles, the road graph, live speeds, and pings. For each, where does it live and how is it read?
Compare with a model answer
Tiles are immutable files keyed by version, zoom, x and y: a blob store behind a CDN. The road graph is built offline and loaded whole into each routing server's memory. Live speeds are a small key-value map from segment to speed. Each navigating phone has an active route the server can re-price. Raw pings are a stream kept for hours, not a table.
Model the data
-- road graph: built offline from map data, shipped as a file, loaded into RAM
junction id INT, lat INT, lng INT -- 8 B packed coordinates (+ id by position)
segment from INT, to INT, segment_id INT,
length_m INT, road_class TINYINT, turn_rules ... -- one row per direction
-- live speeds: key-value, overwritten every minute
live_speed segment_id -> (speed_kmh, samples, updated_at) -- about 16 B each
-- typical speeds: a weekly curve, 672 slots of 15 minutes. Segments share
-- a few thousand curve shapes, so each stores a curve id, not 672 numbers
typical_curve segment_id -> curve_id -- 2 B each
curve (curve_id, slot 0..671) -> speed_kmh
-- active routes: one per navigating phone, deleted when the trip ends
active_route route_id -> (segments, eta_s, better_route) -- a few KB
-- pings: a partitioned stream, kept for a few hours, no user id
ping trip_token, t, lat, lng, speed, headingTiles never change and are read by exact key: files in blob storage behind a CDN. The search reads the graph in random order millions of times a second, so it sits in memory as compact arrays. Only main roads get enough pings for a live speed; for 100 million such segments that is 100 million × 16 B = 1.6 GB. Typical-speed curve ids add 1.2 billion × 2 B = 2.4 GB. Every routing server can hold both. Pings are written once and read once, so they belong in a stream, with a random token per trip instead of a user id.
Sketch the boxes for the three flows: drawing the map, asking for a route, and turning pings into traffic that the next route uses.
Compare with a model answer
Tiles go phone, CDN, tile store. Routes go phone, gateway, routing servers that hold the graph and the live speeds, and each route is saved as active. Pings go phone, gateway, a stream, and a traffic aggregator that writes segment speeds. The ping reply reads the active route to see if a better one is waiting.
High-level design
The three flows share only the phone and the gateway, so each can fail alone.
request and replyasynchronous message
Step through a map load, a route, and a jam that becomes a faster route.
1Mia opens the app. About 20 tiles fill the screen, most from the phone's cache or the CDN edge.
2One tile is a miss. The CDN fetches it from the tile store once and keeps it for everyone after her.
3She asks for a route. Any routing server can answer: each holds the whole graph and the latest speeds in memory.
- trip time
- 10 min
4The search settles about 50,000 junctions instead of 20 million, thanks to precomputed shortcuts (see the deep dive). About 50 ms. ETA: 10 minutes at current speeds.
- trip time
- 10 min
5The route goes back with turn steps and is saved as her active route. The phone now follows it on its own, even if the network drops.
- trip time
- 10 min
- pings/s (all phones)
- 1,000,000
6While she drives, the phone sends 5 GPS points every 5 seconds. The gateway puts each batch on the stream, keyed by area, checks her active route for a better one (none yet), and replies.
- trip time
- 10 min
7A crash ahead: phones on that 3 km stretch report 20 km/h instead of 90. The aggregator snaps points to segments and averages each segment.
- trip time
- 10 min
8Once enough phones agree, it writes the new speed. Each minute, routing servers load changed speeds and refresh the affected shortcuts.
- trip time
- 17 min
9Routing servers re-price only the active routes that cross changed segments. Mia's now costs 17 minutes; a route around the motorway takes 12, so it is saved on her active route.
- trip time
- 12 min
10Her next ping, at most 5 seconds later, finds the better route and the reply carries it. No push channel needed.
- speed source
- typical
11Failure: the aggregator is down. Speeds over 5 minutes old are ignored and routing falls back to typical speeds for this hour. Routes still work; ETAs are less sharp.
Deep dives
Map tiles
The world is cut into squares. At zoom 0 one square holds the whole world; each zoom level splits every square into four. A tile is named by z/x/y, so the phone can work out which tiles cover the screen without asking anyone.
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Pre-rendered image tiles | The phone just shows pictures. Works on any device. | About 1.8 PB to street level. Labels cannot rotate with the car; a style change means rendering everything again. | Old or very weak clients, or a satellite photo layer. |
| Vector tiles to zoom 14 Usual default | About 3.6 TB for the world. The phone draws, so it zooms past 14, rotates and switches to night colours with no new data. | The phone does the drawing. The builder must simplify shapes per zoom so low-zoom tiles stay small. | A modern app. The usual answer. |
Routing on a huge road graph
A route is a shortest path on a weighted graph, where the weight of a segment is its travel time. Dijkstra's algorithm is correct, but it grows a circle of settled junctions around the start until it reaches the destination. For a long trip that circle can hold tens of millions of junctions. Suppose it is 20 million at about 1 µs each (a heap operation plus a memory miss): 20 seconds per route, against a goal of 200 ms.
Searching from both ends, or aiming at the target (A*), shrinks the circle a few times. Not enough: most of the work must happen before anyone asks.
In outline: cut the graph into cells along natural borders, so each cell has few border junctions. For each cell, precompute the fastest time between every pair of its border junctions; these become shortcut edges. A query runs Dijkstra on real roads in the start and end cells and on shortcuts everywhere else. With several levels of cells, a long query settles tens of thousands of junctions, not tens of millions, and the answer is still exact for the current weights.
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Dijkstra or A* on the raw graph | Simple. Any weight change takes effect at once. | Seconds per long route; 3,500 routes a second would need tens of thousands of cores. | A small graph, such as one city or a campus. |
| Contraction hierarchies | Very fast queries. Junctions are ranked by importance and shortcuts skip the unimportant ones. | In the basic form, which shortcuts exist depends on the weights; new speeds mean a rebuild. | Weights rarely change, for example a walking or cycling router. |
| Cells with border shortcuts Usual default | Fast queries, and a speed change only means recomputing the shortcuts of the cells it touches, which runs per cell in parallel. | Slower queries than contraction hierarchies, and a careful partitioning job. | Live traffic changes weights every minute. The usual answer here. |
Capacity check: 50,000 junctions at 1 µs is 50 ms of one core, and 3,500 routes a second × 0.05 s = 175 busy cores: about 20 servers of 16 cores keeps them near half busy. Each holds the 18 GB graph, so they are replicas, not shards. Sharding the graph would turn one search into many network hops.
From pings to traffic and ETAs
A GPS point says where a phone is, not which road. The aggregator first map-matches: it snaps each run of points to the most likely chain of segments, using heading and speed to tell a motorway from the road under it. Then it keeps each segment's speeds from the last few minutes.
- Partition by area. The stream is keyed by map tile, so one aggregator owns an area and averages it alone.
- Ignore noise. One car stopping for fuel is not a jam. Publish a new speed only when several phones agree.
- Blend with history. With few samples, use the typical speed for that segment and 15-minute slot of the week.
The ETA is the sum of segment times. Mia's route before and after the crash:
| Segment | Length | Speed before | Minutes | Speed after | Minutes |
|---|---|---|---|---|---|
| City street | 1.2 km | 30 km/h | 2.4 | 30 km/h | 2.4 |
| Motorway | 6 km | 90 km/h | 4.0 | 90 km/h | 4.0 |
| Motorway, crash | 3 km | 90 km/h | 2.0 | 20 km/h | 9.0 |
| Exit road | 0.8 km | 30 km/h | 1.6 | 30 km/h | 1.6 |
| Total | 11 km | 10.0 | 17.0 |
A senior refinement: live speed suits the next few minutes, but a segment you reach in 50 minutes should use the typical speed for that later time.
Rerouting
Two different events lead to a new route, handled in different places.
- The driver leaves the route. The phone notices several points in a row more than, say, 50 m off the line, and asks for a route from where it is. Local detection is fast and works on a bad connection.
- Traffic makes another route faster. Re-pricing all 5 million active routes every 90 seconds would be 55,000 searches a second, 15 times the route traffic. So re-price only routes that cross a segment whose speed changed, and offer an alternative only if it saves enough, say 2 minutes and 10% of the remaining time, so drivers are not flipped back and forth.
Bottlenecks and failures
At 10x (10 million pings a second, 35,000 routes a second), the stream and aggregator add partitions, since areas are independent. Routing adds replicas: 35,000 × 0.05 s = 1,750 cores. The awkward part is the shortcut refresh, which every replica repeats; at that size, compute it once and ship the result to the replicas.
| When this dies | What happens | What you do |
|---|---|---|
| The tile store | Cache misses fail; areas already cached still draw. | Replicate the store across zones; tiles are immutable, so any copy is right. |
| A routing server | Its in-flight requests fail. | The gateway retries on another replica. A new server needs a minute or two to load 18 GB, so keep spares warm. |
| The aggregator | Speeds go stale. | After 5 minutes, routing falls back to typical speeds. Pings wait in the stream, but old ones are worthless; skip ahead on restart. |
| The network | Pings and reroutes stop for that phone. | The phone keeps guiding along its stored route. |
What a strong answer sounds like
| Level | What the answer does |
|---|---|
| Junior | Draws a map service and a route service, says "Dijkstra" for routing, and stores pings in a database. |
| Mid | Serves tiles from a CDN with zoom levels, sees that plain Dijkstra is too slow on a continent and names a precomputation, and sends pings through a stream to compute segment speeds. |
| Senior | Separates the three systems by their shape, explains why live traffic favours cell shortcuts over a full hierarchy, keeps the graph in memory with replicas rather than shards, designs graceful fallback to typical speeds, and handles rerouting hysteresis and herding. |
How do you compute routes for 3,500 requests a second?
Plain Dijkstra settles tens of millions of junctions on a long trip, so I precompute shortest times between each cell's border junctions. A query walks real roads only near the two ends: about 50,000 junctions, 50 ms, 175 cores at peak. The graph is 18 GB, so every server holds all of it.
Why cells and not a full hierarchy, which answers faster?
Because speeds change every minute. With cells I only recompute shortcuts in the cells whose speeds changed. A basic hierarchy picks its shortcuts from all the weights, so new speeds mean rebuilding the whole thing.
Follow-up questions
How do you ship a new version of the map data?
Load the new graph next to the old one, then switch. Routes carry a graph version, so a reroute during the switch uses its route's version.
How would offline navigation work?
The user downloads a region's tiles and graph slice with typical speeds, and the phone runs the same search, without live traffic.
How do you protect people's privacy?
Pings carry a random token per trip, trip ends are trimmed so homes are not revealed, and raw pings are deleted after hours. Only per-segment averages are kept.
How do you know the ETAs are good?
Every finished trip gives a predicted and an actual arrival time. Track the error by region and time of day, and use it to tune how live and typical speeds are blended.
How do you handle road closures and turn restrictions?
Closures are weight changes, set to infinity, and go through the same per-cell shortcut refresh as traffic. Turn restrictions are modelled in the graph itself (edges between segments), so the search can never produce an illegal turn.
After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".