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.

Case study34 minLesson 4 of 8
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

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

The first five minutes
Interviewer

Design a maps and navigation service.

You

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?

Interviewer

Driving only. Assume address search exists and gives you coordinates.

You

What scale? Daily users, routes a day, and how many people navigate at the busiest moment?

Interviewer

About 100 million daily users, 100 million route requests a day including reroutes, and 5 million phones navigating at peak.

You

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.
Estimate: Vector tiles for the whole world

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?

You know
  • 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
  1. Zoom 14 alone: 414 ≈ 268 million tiles
  2. All levels: 1 + 4 + … + 414 = (415 − 1) ÷ 3 ≈ 358 million tiles (each level adds a third more)
  3. 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.

Estimate: Location pings

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?

You know
  • 5 million phones navigating at peak
  • One batch every 5 seconds per phone

Show the working
  1. 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.

Your turn: Define the API

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

HTTP 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.

Your turn: Model the data

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

Graph, speeds and pings
-- 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, heading

Tiles 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.

Your turn: High-level design

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.

Tiles never touch the API servers. Routing servers hold the graph and live speeds in memory and keep each driver's active route; pings flow through a stream into the aggregator that produces the speeds.Phone app to CDN (GET tile); CDN to Tile store (on miss); Phone app to API gateway (route, pings); API gateway to Routing servers (POST /routes); API gateway to Active routes (better route?); Routing servers to Active routes (save, re-price); API gateway to Ping stream (ping batch), asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds (speed/segment); Routing servers to Live speeds (read each min).Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregatorGET tileon missroute, pingsPOST /routesbetter route?save, re-priceping batchspeed/segmentread each min

request and replyasynchronous message

Tiles never touch the API servers. Routing servers hold the graph and live speeds in memory and keep each driver's active route; pings flow through a stream into the aggregator that produces the speeds.

Step through a map load, a route, and a jam that becomes a faster route.

Map, route, traffic, rerouteUse the arrows or ← → keys
  1. Map, route, traffic, reroute, step 1Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDN19 hitsAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator

    1Mia opens the app. About 20 tiles fill the screen, most from the phone's cache or the CDN edge.

  2. Map, route, traffic, reroute, step 2Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDN1 missAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator

    2One tile is a miss. The CDN fetches it from the tile store once and keeps it for everyone after her.

  3. Map, route, traffic, reroute, step 3Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator

    3She asks for a route. Any routing server can answer: each holds the whole graph and the latest speeds in memory.

  4. Map, route, traffic, reroute, step 4Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting servers50 msActive routesPing streamLive speedsTrafficaggregator
    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.

  5. Map, route, traffic, reroute, step 5Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator
    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.

  6. Map, route, traffic, reroute, step 6Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator
    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.

  7. Map, route, traffic, reroute, step 7Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregatorseg 7731: 20
    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.

  8. Map, route, traffic, reroute, step 8Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsseg 7731: 20Trafficaggregator
    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.

  9. Map, route, traffic, reroute, step 9Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesbetter: 12 minPing streamLive speedsTrafficaggregator
    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.

  10. Map, route, traffic, reroute, step 10Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appsaves 5 minCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsTrafficaggregator
    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.

  11. Map, route, traffic, reroute, step 11Phone app to CDN; CDN to Tile store; Phone app to API gateway; API gateway to Routing servers; API gateway to Active routes; Routing servers to Active routes; API gateway to Ping stream, asynchronous; Ping stream to Traffic aggregator, asynchronous; Traffic aggregator to Live speeds; Routing servers to Live speeds.Phone appCDNAPI gatewayTile storeRouting serversActive routesPing streamLive speedsstale: 6 minTrafficaggregatordown
    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.

1 / 11

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.

How to serve the map
OptionGood atBad atPick it when
Pre-rendered image tilesThe 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 defaultAbout 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.

start cell cells in between end cell shortcut: border to border start destination
The search walks every road only inside the start and end cells. In between it hops along precomputed shortcuts between cell borders.

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.

Ways to find a route
OptionGood atBad atPick it when
Dijkstra or A* on the raw graphSimple. 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 hierarchiesVery 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 defaultFast 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:

SegmentLengthSpeed beforeMinutesSpeed afterMinutes
City street1.2 km30 km/h2.430 km/h2.4
Motorway6 km90 km/h4.090 km/h4.0
Motorway, crash3 km90 km/h2.020 km/h9.0
Exit road0.8 km30 km/h1.630 km/h1.6
Total11 km10.017.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 diesWhat happensWhat you do
The tile storeCache misses fail; areas already cached still draw.Replicate the store across zones; tiles are immutable, so any copy is right.
A routing serverIts 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 aggregatorSpeeds 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 networkPings and reroutes stop for that phone.The phone keeps guiding along its stored route.

What a strong answer sounds like

LevelWhat the answer does
JuniorDraws a map service and a route service, says "Dijkstra" for routing, and stores pings in a database.
MidServes 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.
SeniorSeparates 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.
Routing at scaleStrong answer
Interviewer

How do you compute routes for 3,500 requests a second?

You

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.

Interviewer

Why cells and not a full hierarchy, which answers faster?

You

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.

Self-review: maps and navigation0 of 13

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure