System design interview

Design a maps routing service

Road graph search, shortcuts, live traffic overlays, snapping and ETAs.

Overview
ClientProbe streamAPI gatewaySpeedaggregatorRoute cacheRoutingserviceTrafficstoreSnap indexRoad graphOverlaybuilderClientProbe streamphone positionsAPI gatewaySpeed aggregatorRoute cacheRouting serviceTraffic storeSnap indexRoad graphOverlay builderre-weights cells

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. A driver asks for a route from an address in one city to an address in another, about 600 km away.
  2. The gateway passes the request to the routing service.
  3. The service checks the cache for the same pair of start and end areas and the same minute of departure. It misses.
  4. It snaps both points to the nearest road segments, picking the side of the road that matches the driver's heading.
  5. It runs a two-sided search over the road graph, in full detail near each end and across the overlay in between, with current traffic weights.
  6. It builds the best route and one alternative, with geometry, steps and an ETA, and stores the answer in the cache for 60 seconds.
  7. The driver sees the route and the arrival time in well under a second.
  • Client
  • Probe stream: phone positions
  • API gateway
  • Speed aggregator: speed per segment
  • Route cache: popular trips, 60 s
  • Routing service: snap, search, ETA
  • Traffic store: speeds per 2 minutes
  • Snap index: grid of road segments
  • Road graph: cells and overlay
  • Overlay builder: re-weights cells
On this page

In 2 minutes

I would model the roads as a graph held in memory, about 4.2 GB for 100 million nodes and 250 million edges, since plain Dijkstra would settle about half the nodes and take around 10 seconds across a continent. To get to milliseconds, I would precompute structure in advance. Shortcuts, or contraction hierarchies, bring a query down to about 1,000 settled nodes, but they depend on the weights and take 30 minutes to rebuild, which cannot follow traffic that changes every 2 minutes.

So I would use a partition overlay: cut the graph into cells, precompute travel times between each cell's boundary nodes, and when speeds change, recompute only the changed cells, about 6 seconds in our estimate. A query searches in detail near each end and over the overlay between them. Speeds come from phone probes, matched to segments and summarized every 2 minutes with a blend to usual speeds.

A route request checks a 60-second cache, snaps the points to roads through a spatial index, searches, builds one alternative and steps, and corrects the ETA with a model learned from real trips. At 20,000 requests a second and 5 ms each, that is about 100 cores. I would shard by region with a coarse overlay everywhere, for speed and updates and not for capacity.

Hide the answers and test yourself

Step 1 of 6: Clarify the scope

Interviewer asks

Design a service that gives driving directions between any two points on a continent-sized road network, with a travel time that reflects current traffic. A route must come back in under a second, even for a trip of 1,000 km.

Questions to ask first

Ask these before you draw anything. Under each one is why its answer changes the design.

Which travel modes do we support?

Each mode has its own speeds, turn rules and allowed roads, so each needs its own weights, and sometimes its own graph.

How fresh must traffic be?

Speeds that change every two minutes decide which search methods are usable, because most fast methods precompute something from the weights.

Do we need turn-by-turn steps and alternative routes?

Alternatives need several searches or a smarter one, and steps need the geometry and the road names, not just a time.

How far apart can the start and the end be?

A trip across a city can use a small search. A trip across the continent needs a structure that skips most of the roads.

How accurate must the arrival time be?

Good ETAs need a calibration loop against real trips, not just a sum of road speeds.

Requirements

Functional

  • Return a route between two points with distance, time, geometry and turn-by-turn steps.
  • Snap any start or end coordinate to the nearest sensible road.
  • Use current traffic, so a jam on the usual road changes the answer.
  • Offer up to two alternative routes when they are meaningfully different.
  • Reroute a driver who leaves the planned route.

Non-functional

  • p99 route latency under 1 second for any pair of points on the continent.
  • A change in road speeds shows up in new routes within 5 minutes.
  • Median arrival time error under 10% for trips longer than 10 minutes.
  • Alternatives share at most 80% of their roads with the best route.
  • Handles 20,000 route requests a second at peak.

Step 2 of 6: Size it

The graph fits in memory, so the cost is the search algorithm: plain Dijkstra is 50,000 times too slow. Traffic then matters more than size. Weights that change every 2 minutes defeat a hierarchy that takes 30 minutes to build, so we need a structure that re-weights in seconds.

Assumed for the exercise, not facts about a real system:

  • The continent has 100 million junctions (nodes) and 250 million one-way road segments (edges). A compact graph takes 12 bytes for each edge and 12 bytes for each node.
  • One CPU core settles about 5 million nodes a second in a graph search. A whole route request costs about 5 ms of CPU, including snapping, alternatives and building the steps.
  • Adding shortcuts creates about 150 million extra edges, and a search on the result settles about 1,000 nodes.
  • 10 million phones send a position every 10 seconds, at about 50 bytes each. Speeds are refreshed every 2 minutes, and rebuilding a full hierarchy takes about 30 minutes.
  • An overlay splits the graph into 10,000 cells of about 10,000 nodes, each with about 100 boundary nodes. In each 2-minute cycle, about 10% of the cells have a speed change that matters.
Graph in memory
250M × 12 B = 3.0 GB; 100M × 12 B = 1.2 GB
≈ 4.2 GB, plus 150M shortcuts × 12 B = 1.8 GB, so ≈ 6 GB in all
Plain Dijkstra across the continent
About half the nodes are settled: 50M ÷ 5M a second
≈ 10 s on one core
Search with shortcuts
1,000 nodes ÷ 5M a second
≈ 0.2 ms, about 50,000 times faster than plain Dijkstra
CPU at peak
20,000 requests/s × 5 ms
100 cores busy, about 7 machines of 16 cores, before replicas
Probe traffic
10M phones ÷ 10 s = 1M points/s × 50 B
50 MB/s, about 4.3 TB a day
Re-weighting after a cycle
10% of 10,000 cells = 1,000 cells × 0.2 s (100 boundary nodes × 10,000 nodes ÷ 5M/s) = 200 core-seconds ÷ 32 cores
≈ 6 s, against 30 minutes to rebuild a hierarchy

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

A Client sends a route request to the API gateway, which passes it to the Routing service. The service first checks the Route cache for a recent answer to the same trip. On a miss, it uses the Snap index to attach the start and end to nearby road segments, and then searches the Road graph, which it holds in memory with the current traffic weights.

The search does not use plain Dijkstra. The graph is cut into cells, and the routing service searches in full detail only near the start and the end, and uses a small overlay of precomputed distances across each cell in between. The result is a route found in a few milliseconds, with the roads, steps and time attached.

Traffic comes from the phones themselves. Apps in a trip send positions into a Probe stream. A Speed aggregator works out the speed on each road segment for each 2-minute window, and keeps it in the Traffic store. The Overlay builder takes the changes, recomputes only the cells that changed, and pushes the new weights into the road graph.

Other paths through the system

Serve a repeated trip

  1. A second driver asks for almost the same trip in the same minute.
  2. The gateway passes the request to the routing service.
  3. The service builds the key from the start and end squares, about 200 m across, plus the departure minute and the options. This time it hits.
  4. The service adjusts the first and last few hundred metres for the exact points and returns the cached route.
  5. The driver gets the route in a few milliseconds, with no search at all.

Traffic reaches the graph

  1. Phones on a trip send position, speed and heading every 10 seconds into the probe stream.
  2. The aggregator matches each point to the most likely road segment, using the position, the heading and the previous point.
  3. It keeps a median speed for each segment in each 2-minute window, and blends in the usual speed when too few phones reported.
  4. Every 2 minutes the builder finds the cells whose speeds changed enough to matter.
  5. It recomputes only those cells' overlay distances, in seconds, and swaps the new weights into each routing server.

Step 4 of 6: Go deep on the hard part

The road graph and why plain search is too slow

Interviewer asks

How do you find a route across a continent in milliseconds when the road graph has 100 million nodes?

Model the roads as a graph.

Read more: The road graph and why plain search is too slow

Junctions are nodes. Each road segment is a one-way edge with a weight, the travel time. The shortest route is the path with the lowest total weight. Our continent has 100 million nodes and 250 million edges, which fits in about 4.2 GB of memory.

Dijkstra's algorithm grows a disc of settled nodes around the start, always taking the nearest unsettled one, until it reaches the end. For a trip across the continent the disc covers about half of all nodes. Settling 50 million nodes at 5 million a second takes about 10 seconds on one core. Searching from both ends at once helps, but only by a small factor. We need a search that touches about a thousand nodes.

Shortcuts do this by spending time in advance. Rank the nodes by importance. A node on a quiet dead-end street is unimportant, and a motorway junction is important. Then remove nodes one at a time, least important first. When you remove a node v, look at each pair of its neighbours, u before it and w after it. If the best path from u to w goes through v, add a shortcut edge from u to w with the combined weight, and remember v as its middle. If a short search finds an equally good path that avoids v, no shortcut is needed.

This is called a contraction hierarchy. After it, every node has a level, and every shortest path climbs to higher levels and then descends again. A query runs one search forward from the start and one backward from the end, and each may only move to higher levels. The two meet near the top, and the best meeting point gives the answer. In our estimate each search settles about 1,000 nodes in all, 0.2 ms of CPU, which is 50,000 times faster than plain Dijkstra. The shortcuts add about 1.8 GB.

To return the real roads, unpack each shortcut: it knows its middle node, so replace it by its two halves, and repeat until only ordinary edges remain.

The catch is that the shortcuts are computed from the weights. That matters in the next section.

Live traffic and changing weights

Interviewer asks

Speeds change every few minutes. Why does a precomputed hierarchy struggle, and what do you use instead?

Getting the speeds. Each phone sends a position, speed and heading every 10 seconds.

Read more: Live traffic and changing weights

The aggregator matches each point to a road segment, using the previous point and the heading to choose between parallel roads. For each segment and 2-minute window it takes the median speed, and uses the segment's usual speed for that time of the week when fewer than a few phones reported. That is 1 million points a second, or about 50 MB/s.

Why shortcuts break. A shortcut's weight is a sum of road weights, and the decision to add it, or to skip it because a witness path was just as short, was made with the old speeds. A jam on a witness path means a skipped shortcut is now needed. Without it, searches return wrong routes. The simple fix is to rebuild the hierarchy, but that takes about 30 minutes. In 30 minutes there are 15 refresh cycles, so routes would always be answering for traffic that has already gone.

Separate the structure from the weights. A partition overlay keeps the part that does not depend on speeds fixed, and recomputes the rest fast. Once, offline, cut the graph into compact cells, say 10,000 cells of about 10,000 nodes, and group cells into larger ones in levels. The nodes of a cell that have a road to the outside are its boundary nodes, about 100 in each. The overlay then holds, for each cell, the shortest travel times between all pairs of its boundary nodes.

Customizing. When speeds change, recompute only the cells that contain a changed road. For one cell, run a search from each of its 100 boundary nodes inside the cell: 100 searches of at most 10,000 nodes is 1 million settled nodes, or 0.2 s. If 10% of cells changed, that is 1,000 cells and 200 core-seconds. On 32 cores it takes about 6 seconds, and the cells are independent, so it parallelizes perfectly. Then refresh the larger cells that contain them. A routing server swaps in the new weights atomically, so a search never sees half an update.

Querying. Search in full detail inside the start cell and the end cell, and on the overlay for everything between, from both ends. It touches more nodes than a hierarchy, typically a few ms, which fits our budget. A road closure is just a weight of infinity, applied by the same path within a cycle, or sooner by patching its cell.

Serving: snapping, alternatives, ETAs and sharding

Interviewer asks

What else does a route request need, and how do you scale the service?

Snapping. Phones give coordinates, not road IDs.

Read more: Serving: snapping, alternatives, ETAs and sharding

A spatial index, a grid of small squares that list the segments inside them, finds the road segments within about 50 m of a point. Choose among them by distance, by the direction of travel when it is known, and by road class, so a driver on a motorway is not snapped to the service road beside it. The point usually lies in the middle of a segment, so split the segment into two temporary edges, with the point as a node, for this one search.

Alternatives. Find routes that differ from the best one in a useful way. A simple method picks a few via-nodes from the two searches, builds a route through each, and keeps one only if it shares at most 80% of its roads with the best route, is at most about 25% slower, and has no sharp detours. Show at most two.

ETA calibration. A sum of road speeds is not an arrival time. Junction delays, traffic lights and turns add time. Compare the predicted time of finished trips with the times that the phones really took, grouped by road class, region and hour of the week, and learn a correction for each group. Track the median relative error, the aim being under 10%, and test any new model on live traffic in shadow mode before it answers anyone.

Sharding. The graph is about 6 GB with shortcuts, so a continent fits in the memory of one machine. We shard for CPU, for update speed and to scale up to the planet, not for capacity. Each server group holds full detail for the cells of a few regions and the coarse overlay for the rest. Requests go to the group of the start's region, which keeps its working set hot. A long trip uses the upper overlay levels between regions, which are small and held everywhere. Each cell update lands only on the groups that hold that cell.

Caching. Key a route by the grid squares of the snapped start and end, about 200 m across, with the departure minute and the options. A time to live of 60 seconds keeps routes fresh. Commutes and popular trips hit often. On a hit, only the first and last few hundred metres are adjusted for the exact points.

Step 5 of 6: Name the trade-offs

Search structure

A partition overlay that re-weights in seconds over A contraction hierarchy rebuilt every 30 minutes

Traffic changes appear within minutes. The cost is a search that settles more nodes, a few ms instead of 0.2 ms.

Traffic freshness

A 2-minute refresh with a blend to usual speeds over A refresh every few seconds

Medians over 2 minutes are stable, and the update work stays small. The cost is that a sudden crash takes a couple of minutes to show.

Route cache

A 60-second cache keyed by start and end squares over No cache, or a cache for a long time

Repeated trips cost almost nothing to serve. The cost is a slightly stale answer, and a lower hit rate for trips between unusual points.

Sharding

By region, with a coarse overlay everywhere over A full copy of everything on every server

Each server's working set stays small and hot, and updates reach fewer machines. The cost is routing requests by region and extra care for trips that cross a boundary.

Step 6 of 6: Say what breaks next

Questions an interviewer asks once the design is on the board. Answer each one out loud first.

How do you handle turn restrictions and costs for turns?

Search over road segments instead of junctions, so a node means 'arriving on this road' and an edge means 'turn onto that one'. A banned turn is then a missing edge, and a left turn across traffic is an edge with extra cost. The graph grows several times larger, and everything else stays the same.

How would you route for a departure an hour from now?

Keep several weight sets, for the current speeds and for predicted speeds at later times, learned from the same history by time of week. Pick the set for the departure time. A long trip can use a time-dependent cost, where each segment's weight depends on when you reach it, at the price of a slower search.

How would you add walking or cycling?

Use a separate set of weights, and often a separate graph, for each mode, since the allowed roads and speeds differ. Share the map data and the snapping index. Each mode has its own overlay and its own memory, so add modes only as the product needs them.

How do you detect that a driver left the route?

The app compares each position with the planned geometry. If the driver is more than about 50 m off it for a few seconds, the app asks for a new route that starts at the current position and heading. The cache helps here too, since many drivers take the same wrong turn.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.