System Design Interview

Google Maps System Design Interview Guide

A shortest path across a continent is a graph search over hundreds of millions of edges. You have 200 milliseconds, and the edge weights changed while you were reading this sentence.

L4, A working router and a tile pyramid L5/L6, Preprocessing vs live traffic L7/L8, Freshness pipelines, probe privacy, cost
A stick figure at a desk holding a magnifying glass over a vast folded paper map while a robot beside them hands over a single pre-drawn red line between two pins
01

What the interviewer is testing

Nearly every other system design question is about moving data between machines. This one is about a computation. The data barely moves at all — the road network of the entire planet fits in the memory of one ordinary server, and it changes slowly. What makes the question hard is that the answer a user wants is the result of a graph search that, run honestly, takes seconds.

The hard problem sits in one sentence. A shortest-path search over a continental road network is nearly two orders of magnitude too slow to run inside a web request, and the only way to make it fast is to precompute — but the weights on the graph change every minute. Those two facts pull in opposite directions, and the entire design is the resolution between them. Precomputation buys a query that settles a few hundred nodes instead of nine million. It buys it by baking assumptions about edge weights into a structure that takes ten minutes to rebuild. Live traffic invalidates those assumptions continuously. An answer that reaches "use Dijkstra with a priority queue" has not started; an answer that reaches "precompute shortest paths" and stops has not noticed that it just made the traffic feed useless.

The second thing being tested is whether you notice there are two systems here, not one. Drawing the map is not the same problem as routing across it. Tiles are immutable, enormously cacheable, and read by everyone; routes are unique per user, uncacheable, and computed fresh. They share a data source and almost nothing else, and a candidate who gives both the same architecture is telling the interviewer they have not looked closely.

Notice what the question is not about. It is not "find the twenty restaurants near me", which is a spatial index over points and is covered in designing proximity search. Geohash and S2 answer which things are near this point; they say nothing about how to get from this point to that one, because road distance is not a function of straight-line distance and a river with no bridge is invisible to a geohash. This post uses a spatial index exactly once, to snap a pair of coordinates onto the graph, and then never again. It is also not the matching problem from the ride-sharing guide: Uber's hard question is pairing supply with demand and tracking moving drivers, and it calls a routing engine as a black box for ETAs. This post is that black box. Where the two touch — live location ingest — the ride-sharing post cares about the freshness of one driver's position, and this one cares about what a million anonymous traces say about the speed of one road segment.

Level Core question Differentiator
L4 Can you model roads as a weighted graph and route on it? Knows a road network is a directed weighted graph, not a grid; picks Dijkstra or A*; serves map imagery from a zoom-level tile pyramid behind a CDN.
L5 Can you get the query under 200 ms, and say what you paid for it? Recognises that plain Dijkstra is seconds, not milliseconds; reaches for hierarchical preprocessing; can state the preprocessing time and index size it costs.
L6 What happens to that preprocessing when traffic changes? Sees the conflict between baked-in weights and live speeds; separates topology preprocessing from metric customisation; treats the ETA as time-dependent rather than a sum of current speeds.
L7/L8 Who owns the freshness pipeline, and what does it cost? Designs the probe-to-weight loop end to end, including map matching and the point at which user traces stop being personal data; knows which component dominates spend and what degrades first when the pipeline stalls.
ℹ

Scope note. This guide covers driving routes on a road graph, the tile pyramid that renders the map, and the traffic loop that keeps both current. Public-transit routing is left out deliberately: it is a genuinely different algorithm family (RAPTOR and Connection Scan over timetables rather than Dijkstra-descendants over a graph) and deserves its own treatment rather than a paragraph.

02

Requirements: two products sharing one map

The functional list is short, and worth reading for what it implies about load rather than what it says. Two of these five are read paths that every user hits constantly; one of them is a background pipeline that never faces a user and yet sets the quality of everything else.

  • Render the map. Serve map geometry for any location on earth at any zoom level, fast enough to pan and zoom without visible tearing.
  • Route between two points. Given an origin, a destination, a departure time and a travel mode, return the best route with a distance, an ETA, and turn-by-turn instructions. Offer two or three meaningfully different alternatives.
  • Navigate. Follow a user along a route, detect when they have left it, and re-route. This is the same routing call at a much higher frequency and a much lower tolerance for latency.
  • Reflect live conditions. Congestion, incidents and closures must change both the ETA and the chosen route, not just a colour on the screen.
  • Learn from traffic. Turn anonymous position traces from devices into per-segment speeds, and per-segment speeds into the weights the router uses.

The non-functional targets below are where the design actually comes from. Each one is referenced by at least one decision later; the accordions say which.

Requirement Target Why this number
Route query latency p95 < 200 ms end to end The threshold at which tapping "Directions" feels like a lookup rather than a job
Re-route latency during navigation p95 < 500 ms, and never blocking A driver who has missed a turn is moving; the old instruction is already wrong
Tile latency p95 < 50 ms at the edge Panning fetches a dozen tiles at once; each one is on the render path
Traffic freshness Probe observation to routing weight in < 2 min A jam that takes five minutes to appear has already redirected nobody
ETA accuracy Within 10% of actual on 90%+ of trips The ETA is the product; a route that is right but arrives late is a wrong answer
Availability 99.99% for routing, 99.99% for tiles 52 minutes a year; navigation failures land on people who are already driving
Map data freshness New roads within weeks; closures within minutes Geometry changes slowly and is expensive to rebuild; conditions change fast and must not wait for a rebuild

What each target forces

Route p95 under 200 ms ›

Two hundred milliseconds is the whole round trip, including the mobile network. §6 decomposes it and finds roughly 40 ms available for server work once the wire is paid for. That figure is the single most consequential number in this design, because a bidirectional Dijkstra over a continental graph is between one and four seconds. The gap is not a few percent and it is not a factor of two; it is fifty to a hundred times, and nothing about faster hardware or a bigger fleet closes it.

What this drives The entire existence of §5. A gap this size can only be closed by moving work from query time to preprocessing time, which means accepting a preprocessing stage, an index that must be built, shipped and versioned, and a rebuild cost whenever the graph or its weights change.
Traffic freshness under two minutes ›

Freshness here means the full loop: a device observes it is moving at 8 km/h on a road whose free-flow speed is 60, that observation is matched to the correct road segment, aggregated with others, turned into an edge weight, and reflected in the next route computed anywhere in the region. Two minutes is not a comfort target. Congestion builds over five to ten minutes, so a loop slower than a couple of minutes reports jams that have started to clear and routes people into them.

This target is what makes the problem interesting rather than solved. Every classical speedup technique in the routing literature assumes a static metric, and the good ones take minutes to preprocess. A two-minute freshness requirement and a ten-minute preprocessing cost cannot both hold.

What this drives The split in §5 between metric-independent preprocessing (done once per map version, hours) and metric customisation (done per traffic update, about a second). It also drives the separate live-speed array in §7, which is swapped atomically rather than rebuilt.
Tile p95 under 50 ms at the edge ›

A single screen of map is a dozen to twenty tiles, requested in parallel and all of them on the critical path for the first paint. Fifty milliseconds is achievable only from a nearby cache: a cross-region round trip alone is 70–100 ms (§ Numbers to know), so any design where a meaningful fraction of tile requests reach a central origin has already missed the target.

What this drives Tiles must be immutable and content-addressed so they can be cached indefinitely at the edge. That in turn forces map versions to be baked into the tile URL rather than invalidated, which is §8's cache key design, and it is why a map release is a slow, staged rollout rather than a flip.
ETA within 10% on 90% of trips ›

Accuracy is stated as a distribution, not a mean, because the mean is easy and useless — a router that is 20% early half the time and 20% late the other half has a perfect mean error and an unusable product. The asymmetry matters too: arriving five minutes early is a mild surprise, arriving five minutes late can mean a missed flight, so in practice these systems are tuned to be slightly pessimistic.

What this drives The ETA cannot be a sum of current speeds. A 40-minute drive crosses roads you will not reach for half an hour, and using their current speed is a prediction that traffic is frozen in time. §6 evaluates each edge against the time you are predicted to arrive at it, blending live speed near the origin into historical profiles further out.
99.99% availability, and what degradation looks like ›

52 minutes a year. The useful part of this target is not the number but the shape of the failure, because this system has an unusually graceful degradation path and an interviewer will look for it. If the traffic pipeline dies, routing continues on historical speed profiles and the product is worse but correct. If tile origins die, the edge serves the previous map version indefinitely. If routing dies, there is no fallback — which is why routing servers are stateless replicas of an artifact, the cheapest possible thing to over-provision.

What this drives The last-known-good artifact model in §9 and §10: every input to routing has a previous version that is still servable, and no component is allowed to fail in a way that leaves the router with nothing.
Two clocks for map data: weeks for geometry, minutes for conditions ›

These are separate requirements that look like one. A new housing estate appearing in the road network is a change to the graph's topology: it adds nodes and edges, invalidates the preprocessing, and requires a full rebuild and a staged rollout. A road closed by a burst water main is a change to a weight, and must take effect in minutes without touching topology.

Collapsing them is the common mistake. If closures go through the geometry pipeline, they arrive the following week. If geometry goes through the live path, every rebuild is an emergency.

What this drives Closures are modelled as an extreme metric value (an effectively infinite traversal cost) rather than an edge deletion, so they ride the same fast path as congestion. §7 keeps topology and metric in physically separate arrays for exactly this reason.
03

Capacity estimation: the graph is small, the pipeline is not

The usual opening move — daily active users times requests each — is the wrong first question here, because the quantity that shapes this design is not a rate at all. It is the size of the road network, and the surprising thing about it is how small it is.

Start from measured ground truth rather than a guess. The standard benchmark road networks in the routing literature are Western Europe at about 18 million nodes and 42 million directed edges, and the continental United States at about 24 million nodes and 58 million directed edges. Both sit close to 2.3 directed edges per node, which is a property of road networks rather than a coincidence: roads meet mostly at three-way and four-way junctions, and a road graph is very nearly planar. Scaling those to the whole routable planet lands somewhere around 150 million nodes, and the estimator below defaults there.

A node is an intersection, not a GPS point. The curve of a road between two junctions is geometry for drawing, not structure for searching, so it lives with the tiles and never enters the graph. That distinction is worth stating out loud in an interview, because a candidate who models every shape point as a graph node inflates the graph by an order of magnitude and then concludes, wrongly, that it must be sharded.

So the dimensions that bind are: the size of the graph, which decides whether a routing server can hold it; the rate of route computations, which is dominated by navigation re-routes rather than by people tapping “Directions”; the tile corpus, which is the actual storage bill; and the probe volume, which is the actual bandwidth bill.

Interactive capacity estimator

150 M
25K
4.0 M
1.0 M
50 M
Whole planet's routing graph
4.56
GB of RAM, one server
(495.00 M edges × 8 B) + (150M × 4 B)
Route computations
158.33 K
searches / sec
25K fresh + 4.0 M ÷ 30 s
Routing CPU
396
cores, before redundancy
158.33 K × 1.5 ms ÷ 60% util
Vector tile corpus
8.05
TB, zoom 0–14
358M tiles × 30% × 75 KB
Tile requests reaching origin
30.00 K
req / sec
1.00 M/s × (1 − 97% edge hit)
Raw probe ingest
20.7
TB / day before aggregation
50 M ÷ 5 s × 24 B × 86,400
✓

The architectural implication: read the teal card and the coral card together. The entire routable planet is 4.56 GB — it fits in RAM on a machine you could rent by the hour, which is why §4 replicates the graph to every routing server instead of sharding it, and why §9's scaling story is not about the routing fleet at all. Roughly 400 cores answer every route request on earth. Meanwhile the probe pipeline moves 20.7 TB a day just to keep the weights on that 4.56 GB current. The expensive part of this system is not computing routes; it is knowing what the roads are doing. Drag the node slider and watch how little the memory figure moves — even a 500-million-node graph is 15 GB, still one machine. Drag the navigation slider instead and the core count moves five times faster than the fresh-query slider does, because a navigating user silently asks for a route every 30 seconds and there are millions of them at once.

ℹ

Constants behind the sliders. 2.3 directed edges per node, measured from the benchmark networks above. 1.0 overlay shortcut edge added per node by the preprocessing in §5 — for contraction hierarchies on road networks the added shortcuts come out close to one per node, and the cell-overlay structure this design actually uses is in the same range. 8 bytes per directed edge in the compressed adjacency layout of §7 (a 4-byte head node id and a 4-byte weight) and 4 bytes per node for its offset into that array; turn restrictions, road names and drawing geometry are not in this figure because they are not in the search graph. 30 seconds between re-route checks during active navigation — a design knob, not physics, and one that trades responsiveness against most of the fleet. 1.5 ms of CPU per route computation, deliberately an order of magnitude above the raw sub-millisecond graph search because the search is the cheap part: snapping, unpacking the path, evaluating a time-dependent ETA over several hundred edges, producing two alternatives and generating instructions dominate it (§6). 60% target core utilisation. Tiles: pre-generated to zoom 14 only, with clients overzooming from there (§8); 30% of tiles containing any feature at all, since the planet is mostly ocean, ice and empty land; 75 KB per compressed vector tile, an urban-average figure that runs closer to 10 KB over farmland. 97% edge cache hit rate, which needs immutable tile URLs (§8) and the heavy popularity skew of tile demand — neither alone would produce it. Probes: one position every 5 seconds per device in motion, at 24 bytes each — a rotating pseudonymous id, latitude, longitude, timestamp, speed and an accuracy estimate — uploaded in batches rather than as individual requests.

⚠

What this estimate deliberately excludes. Rendering. The 8.05 TB is the output corpus; producing it from source data is a batch job over hundreds of gigabytes of geometry that runs for hours across a large fleet, and it repeats on every map release. It appears in none of these cards because no slider here drives it — release cadence does, and that is a business decision rather than a capacity one. An L7 answer names this as the largest single line item in the bill and points out that the lever on it is how much of the corpus a release has to re-render, not how fast the renderer is.

04

High-level architecture: two read paths and one feedback loop

The shape below follows directly from §1's observation that drawing the map and routing across it are different problems. The left half is a classic cacheable read path that ends at a CDN edge for 97 requests in 100. The right half never caches a response at all. Underneath both sits a pipeline that produces the artifacts they serve, and around the whole thing runs the loop that turns user movement back into routing weights.

Google Maps architecture: a cacheable tile path on the left, an uncacheable routing path on the right, both fed by an offline build pipeline, with an asynchronous probe-to- weight feedback loop along the bottom Client / Map SDK renders tiles, follows the route CDN edge 97% hit, immutable URLs Regional gateway auth, quota, region pinning Tile origin serves pre-built vector tiles Routing service graph in RAM, 4.6 GB Snap & geocode coords → graph nodes Versioned artifact store graph + overlay topology + tile corpus, immutable per release Probe ingest 20.7 TB/day Map matcher trace → road segments Speed aggregator per-edge live speed Customisation re-weights the overlay Offline map build compile graph, render tiles 3% miss snap load at release position batches new weights, ~every 60 s on the request path asynchronous or offline
Figure 1 — Tiles are served from the edge and routes are computed on demand; neither the build pipeline nor the traffic loop is on a user's request path.

Client / Map SDK. Fetches vector tiles and draws them locally, which is why zoom and pan feel instant even though the corpus stops at zoom 14. It also holds the full route geometry after a single request, so ordinary navigation needs no network at all between re-route checks. The non-obvious constraint is that it is also a sensor: the position batches it uploads are the input to the traffic pipeline, which means a client bug can degrade routing for everyone in a city.

CDN edge. Terminates almost all tile traffic. The constraint that makes 97% achievable is that tile URLs are immutable — the map version is part of the path, so a tile is never invalidated, only superseded. A design that PUTs new tiles at the same URL trades that hit rate for an invalidation storm on every map release.

Regional gateway. Authenticates, applies per-key quotas, and pins the request to a regional routing cluster. Quota enforcement matters more here than in most systems because the cost of requests varies by three orders of magnitude between a tile and a distance-matrix call (§10b).

Tile origin. A static file server in front of the tile corpus, handling the 3% of requests the edge misses. It computes nothing: every tile it returns was rendered hours or weeks earlier by the build pipeline. Treating it as a renderer is the common design error, and it turns an 8 TB storage problem into an unbounded compute problem.

Routing service. Holds the entire road graph and its overlay in memory and answers searches. It is stateless in the sense that matters — any replica can answer any query — but it is not small: process start means loading several gigabytes, so replicas are added in minutes, not seconds, and the fleet is provisioned for peak rather than autoscaled into it.

Snap & geocode. Converts a coordinate pair into the graph node the search must start from. This is the one place the spatial-index machinery from the proximity search guide appears, and it is deliberately a separate service because it is the only component whose index is organised by geography rather than by graph structure.

Versioned artifact store. Holds the compiled graph, the overlay topology and the tile corpus, each immutable under a release id. Everything upstream produces artifacts; everything downstream loads them. The constraint is that a release is not atomic across the fleet — servers on the old and new version coexist for as long as the rollout takes, which §10 turns into a real failure mode.

Probe ingest, map matcher, speed aggregator. The pipeline that turns raw positions into per-edge speeds: a durable log absorbs the batches, the matcher decides which road each trace was actually on, and the aggregator combines matched traversals into a speed per segment. The constraint here is statistical rather than architectural — a segment with three observations cannot be distinguished from noise, which is what §10b's minimum-contributor threshold is for.

Customisation. Takes the new speeds and recomputes the overlay weights, then publishes them to every routing replica. This is the component that makes live traffic compatible with precomputation, and §5 is mostly about why it has to exist.

Architectural rationale

Why every routing server holds the whole planet ›

The reflex when a dataset is large is to shard it, and geography offers an obvious key. It is the wrong move here, for a reason that is specific to shortest-path search: a query does not know in advance which parts of the graph it needs. A route from Lisbon to Warsaw touches a dozen countries, and — worse — the optimal route may leave and re-enter a region that a naive partition would have excluded. Sharding by geography turns a single in-memory search into a distributed search with cross-shard boundary negotiation at every border, and the network hops alone would blow the 40 ms server budget from §6.

Because §3 puts the whole graph at 4.56 GB, the question never has to be asked. Replication is strictly better: every replica answers every query, failure of one costs nothing, and adding capacity is adding a machine.

Tradeoff Every replica pays the full memory cost and the full load time, so a map release is a fleet-wide multi-gigabyte rollout. That is a real operational burden — it is the reason releases are staged over hours in §9 — but it is paid on a cadence you control rather than on every query.
Alternatives Geographic sharding with boundary nodes Hierarchical split: long-distance graph replicated, local graphs sharded
Why tiles are pre-rendered rather than drawn on request ›

Rendering on request is attractive because it removes an 8 TB corpus and makes styling changes instant. It fails on the arithmetic of §3: a million tile requests per second, each requiring a spatial query plus a geometry simplification, is a compute bill that dwarfs the rest of the system combined — and it is compute spent re-deriving the same answer, because tile content is identical for every user who looks at that square.

Pre-rendering also decouples the two freshness clocks from §2. A map release can be built, validated and rolled back as a unit; a renderer that reads live data has no such boundary, and a bad geometry import would be visible worldwide within seconds.

Tradeoff Styling is frozen into the artifact. Vector tiles recover most of this — they carry features and attributes rather than pixels, so the client can restyle, switch to dark mode, or toggle layers without a new fetch — which is precisely why the corpus is vector and not raster.
Alternatives On-demand raster rendering with an aggressive cache Hybrid: pre-render low zooms, render high zooms on demand
Why the traffic loop is asynchronous, and what that costs ›

Nothing in the probe pipeline is on a user's request path, and that is a deliberate isolation rather than an accident of implementation. Map matching is a search of its own — §6 describes it — and running it inline would put a second expensive algorithm inside the 200 ms budget for no benefit, since one device's trace is meaningless until it is combined with others.

The cost is the freshness floor in §2. Ingest batching, matching, aggregation windows and overlay customisation each add latency, and two minutes is the sum of them rather than any one being slow. An interviewer who pushes on "why not thirty seconds?" is asking you to name which term you would shrink: the honest answer is the aggregation window, and the honest cost is a noisier speed estimate from fewer observations.

Tradeoff Asynchrony means the router is always acting on a slightly stale world. This is acceptable because congestion has minutes-long dynamics, and it would not be acceptable if the weights represented something that changed in seconds.
Alternatives Inline matching on ingest Push incident reports straight to a closure overlay, bypassing aggregation
Why snapping is its own service ›

Snapping answers "which road segment is this coordinate on", which is a nearest-neighbour query over geometry, not a graph search. It wants an entirely different index — a spatial one, over road segment shapes — and that index is also what map matching needs in bulk. Keeping it separate lets the routing servers hold only what a search touches, and lets the matcher scale on its own schedule, which matters because the matcher's query volume is an order of magnitude above the interactive one.

Tradeoff A network hop inside the 40 ms server budget, about 5 ms in §6. Co-locating the index in the routing process would save it at the cost of several more gigabytes per replica and a second index to keep in step with releases.
Alternatives Spatial index embedded in the routing binary Client-side snapping against the tile the client already has
Why the artifact store is versioned and immutable ›

Every serving component in this design loads a file rather than querying a database. That is unusual enough to be worth defending: it means the serving path has no dependency on the systems that produce map data, so an outage in the import pipeline is invisible to users, and a bad build is a rollback rather than a repair.

Immutability is what makes the tile cache key work and what makes a routing replica's state describable by a single version string. When something is wrong in production, the first question — "which version is this server on?" — has an exact answer.

Tradeoff Storage for several retained versions of an 8 TB corpus, and a rollout that is slow by construction. The alternative, mutating a live datastore, makes every change instant and every mistake instant too.
Alternatives Live geospatial database queried at request time Incremental patch artifacts layered on a base release

How real systems differ

The engines below are all in production and all make different choices, which is the useful part — it shows which decisions are genuinely open rather than settled. Two caveats on the Google column: its routing internals have never been published, so those cells are inference from observed behaviour rather than documented fact, and the customisable-route-planning work itself came out of Microsoft Research and is documented as shipping in Bing Maps. Saying that out loud is worth more in an interview than asserting an architecture nobody outside Google has seen.

Decision This design Google Maps OSRM Valhalla
Speedup technique Cell-overlay preprocessing with separate metric customisation Undocumented; behaviour is consistent with this family, but Google has published on ETA prediction, not on its speedup structure Offers both contraction hierarchies and a multi-level Dijkstra pipeline Hierarchical A* over a tiled graph, no full contraction
Response to changed weights Re-run customisation, seconds, topology untouched Continuous customisation from live traffic Contraction hierarchies need a rebuild; the multi-level pipeline supports fast re-weighting Weights read at query time, so live traffic needs no rebuild at all
Graph residency Whole planet in RAM, replicated Regional serving is observable from latency; the residency model is not public Whole extract in RAM, memory-mapped Tiles loaded on demand, so memory scales with traffic not with the planet
Cost of that choice Fast queries, a customisation pipeline to operate Fastest queries, the most machinery Fastest queries in the contraction mode, but a static metric Flexible and cheap on memory, queries an order of magnitude slower
✓

Valhalla's row is the one worth dwelling on, because it is the counterexample to this whole design. Reading weights at query time makes live traffic trivial — there is nothing to customise — and costs perhaps 10–50× on query latency. That is a bad trade at the scale in §3 and an excellent one for a self-hosted engine serving thousands of queries a day rather than hundreds of thousands a second. The choice follows from the requirements, not from one technique being better.

05

From Dijkstra to a routable planet: the speedup ladder

There is one dominant algorithm here rather than a menu of comparable options, and the way to present it is as a ladder: start with the textbook answer, measure why it fails, and climb until something meets the budget. Each rung buys an order of magnitude and charges for it in a different currency — memory, preprocessing time, or flexibility — and the last rung is chosen not because it is fastest but because it is the only one compatible with §2's traffic requirement.

Rung 1: Dijkstra, and the size of the problem

Dijkstra's algorithm explores outward from the origin in order of increasing distance, settling every node closer than the destination before it reaches it. On a road network that is a roughly circular region centred on the origin with the destination on its rim, so a cross-continent query settles something close to half the graph. On the 18-million-node Europe benchmark that is around nine million nodes, and the measured runtime is on the order of seconds — call it two, with a good priority queue and a warm cache.

Against a 40 ms server budget (§6), that is not a factor of two out. It is a factor of fifty. No amount of profiling recovers it.

Rung 2: search from both ends

Running a second search backwards from the destination and stopping when the two frontiers meet halves the radius of each circle. Because the explored area grows with the square of the radius, two circles of half the radius cover about half the area of one full circle — a factor of two, occasionally a little better. Useful, free, and nowhere near enough.

Rung 3: aim the search

A* adds a lower-bound estimate of the remaining distance to each node's priority, which stretches the explored region into an ellipse pointed at the destination. The obvious estimate — straight-line distance divided by the maximum speed limit — is a legal lower bound but a weak one, because on a fast-road network the ratio between straight-line distance and travel time varies enormously. The strong version, usually called ALT, precomputes exact distances to a few dozen well-chosen landmark nodes and uses the triangle inequality to derive much tighter bounds. That gets a continental query into the low hundreds of milliseconds.

Notice what just happened: the first genuinely large win required precomputation. From here on, every rung is a different answer to the question of what to precompute.

Rung 4: contraction hierarchies, and why they are not the answer here

Contraction hierarchies take the precomputation idea to its conclusion. Order every node by how “important” it is to shortest paths — a residential cul-de-sac is unimportant, a motorway junction is not — then remove nodes one at a time from least to most important. Each time a node is removed, check whether any path through it was the unique shortest path between two of its neighbours; if so, insert a shortcut edge carrying that path's total weight so no distance is lost. The result is the original graph plus roughly one shortcut per node, arranged so that every shortest path can be walked as an upward sequence followed by a downward one.

A query then runs bidirectionally and only ever moves up the hierarchy, which prunes the search to a few hundred nodes. Measured queries on the Europe benchmark are around a tenth of a millisecond — four orders of magnitude faster than rung 1, on the same graph, for the same answer.

Four search-space shapes: Dijkstra explores a full circle around the origin, bidirectional search explores two smaller circles, A* with landmarks explores an ellipse aimed at the target, and hierarchical preprocessing explores a thin corridor 1 · Dijkstra s t ~9 M nodes ~2 s no preprocessing 2 · Bidirectional s t ~4 M nodes ~1 s no preprocessing 3 · A* + landmarks s t ~100 K nodes ~100 ms landmark distances 4 · Hierarchical s t ~300 nodes ~0.1–2 ms minutes to hours
Figure 2 — The same query on the same continental graph. Orders of magnitude are bought by shrinking the explored region, and every large win is paid for with precomputation. Figures are indicative of the published Europe benchmark, not guarantees.

So why is rung 4 not the answer? Because of one sentence in §2. The node ordering and every shortcut weight are functions of the metric. Which nodes are important depends on which roads are fast; what a shortcut costs depends on the travel time of the path it replaces. Change the speed of one motorway and the shortcuts through it are wrong. Rebuilding the hierarchy for a continental graph takes minutes. Traffic changes every minute. The two requirements are incompatible, and a candidate who stops at contraction hierarchies has built a routing engine that cannot use the traffic system they just designed.

Rung 5: separate the topology from the weights

The resolution is to split the preprocessing into a part that depends only on the shape of the graph and a part that depends on the weights, then make the second part cheap. This is the customisable-route-planning approach, and it is the one this design uses.

Partition the graph into cells of a few thousand nodes each, chosen so that as few edges as possible cross cell boundaries. The nodes sitting on those boundaries are the only ones that matter for travel between cells. Build an overlay graph whose vertices are exactly those boundary nodes. It carries two kinds of arc: one between every pair of boundary nodes in the same cell, standing for the best path through that cell's interior, and the original road arcs that cross a cell border, which are what let a search move from one cell to the next. That structure — which cells exist, which nodes are on their borders, which overlay edges exist — depends only on topology, and is built once per map release.

The weights on those overlay edges are a different matter: each one is the shortest-path distance between two boundary nodes through the interior of one cell. That does depend on the metric, and recomputing it is embarrassingly parallel, because each cell's interior can be searched independently of every other cell. That step is customisation, and for a continental graph it completes in about a second across a modest number of cores.

The road graph is partitioned into cells; boundary nodes on the cell borders become the vertices of a much smaller overlay graph whose edge weights are recomputed whenever traffic changes The partitioned road graph 150 M nodes, cells of a few thousand cell A cell B cell C cell D ● boundary nodes — the only ones that matter between cells build once per release The overlay graph boundary nodes only, with one edge per pair inside a cell - - - overlay edge weights: recomputed per traffic update, ~1 s, cell-parallel Topology is expensive and rarely changes. Weights are cheap and change constantly. Splitting them is the whole trick.
Figure 3 — Metric-independent preprocessing builds the cell partition and the overlay's shape; customisation recomputes only the dashed weights. Only cells containing a changed road need redoing.

A query now runs bidirectionally over a mixed graph: real edges inside the origin's cell, then overlay edges to skip whole cells at a stride, then real edges again inside the destination's cell. It settles a few thousand nodes instead of a few hundred, so it lands at one to two milliseconds where contraction hierarchies land at a tenth of one. That is the price, and against a 40 ms budget it is a price worth paying — 2 ms of a 40 ms budget buys the entire traffic system.

ℹ

Worked example, in miniature. Take a cell whose boundary has four nodes, b1–b4, and three thousand interior nodes. The overlay needs the shortest interior distance for each ordered pair — twelve values. Computing them is four searches, one rooted at each boundary node, each confined to three thousand nodes: microseconds. Multiply by the number of cells and divide by the cores available, and a continental customisation is about a second. Now suppose traffic slows one street inside that cell. Exactly twelve values change, and no other cell is touched at all. Compare with contraction hierarchies, where that same street may have contributed to shortcuts anywhere in the hierarchy above it, and the safe response is to rebuild everything.

Query pseudocode: bidirectional search over the overlay ›

The search itself is still Dijkstra. What changes is which edges are visible from a node: inside the source or target cell you see real road edges, and everywhere else you see only overlay edges, which is what makes the stride large.

def route(src, dst, metric):
    # metric: the per-edge weight array published by customisation (§7)
    src_cell, dst_cell = cell_of[src], cell_of[dst]

    def edges_from(v):
        # inside the endpoint cells, walk the real road graph
        if cell_of[v] in (src_cell, dst_cell):
            yield from road_edges(v, metric)
        if is_boundary[v]:
            # jump boundary-to-boundary across a whole cell interior...
            yield from overlay_edges(v, metric)
            # ...and take the real arcs that cross into the next cell
            yield from cut_edges(v, metric)

    fwd = Dijkstra(src, edges_from)
    bwd = Dijkstra(dst, edges_from, reverse=True)
    best, meet = INF, None
    while fwd.min_key() + bwd.min_key() < best:      # standard meeting condition
        side = fwd if fwd.min_key() <= bwd.min_key() else bwd
        v = side.settle_next()
        if fwd.seen(v) and bwd.seen(v) and fwd.dist[v] + bwd.dist[v] < best:
            best, meet = fwd.dist[v] + bwd.dist[v], v
    return unpack(meet)                # expand overlay edges into real roads

unpack is the step that turns a path containing overlay edges into a real sequence of roads the user can be told to drive. Each overlay edge is expanded by re-running a search confined to its cell, or by reading a stored path if the build kept one. That expansion is why §3 budgets 1.5 ms of CPU per route rather than the raw search time.

Why not precompute every pair, or use hub labelling ›

All-pairs is the first idea everyone has and it dies on arithmetic: 150 million nodes squared is 2.25 × 1016 pairs, which at one byte each is 22 petabytes — and one byte cannot hold a travel time, so a usable table is four times that. The killer is not the size, which a hyperscaler could store: it is that every byte of it is a function of the metric, so the whole table would have to be rebuilt every time traffic moved.

Hub labelling is the serious version of the same instinct, and it is genuinely the fastest known technique — each node stores distances to a carefully chosen set of hubs, and a query is a merge of two sorted lists with no graph search at all, in well under a microsecond. Two things rule it out here. The index is large, tens of gigabytes for a continental graph where the overlay is single-digit, and it is entirely metric-dependent, so it inherits exactly the rebuild problem that disqualified contraction hierarchies. It is the right choice when the metric is static and queries are enormous in volume — distance matrices for logistics planning, for instance.

Tradeoff Roughly 1,000× faster queries for roughly 10× the memory and no compatibility with live traffic.
Alternatives Hub labelling for static metrics Transit-node routing for long-distance queries only
How the cell partition is chosen, and why it matters ›

Customisation cost is driven by the number of boundary nodes, because the overlay has one edge per ordered pair of boundary nodes in a cell — quadratic in the boundary size. So the partition wants cells that are balanced in size and, above all, have few edges crossing between them. That is graph partitioning, and road networks are unusually friendly to it: they are nearly planar, and natural barriers like rivers, coastlines and motorway corridors are genuinely sparse cuts.

A grid partition by latitude and longitude cuts straight through dense city centres, exactly where the edge count is highest, so it leaves more boundary nodes than a partition half its size. The practical approach is multi-level: a few thousand nodes per cell at the lowest level, with cells grouped into larger cells above, so a long-distance query strides at a coarser level still.

Tradeoff Smaller cells mean faster queries and a bigger, slower-to-customise overlay. This is the knob to name when an interviewer asks what you would tune.
?

The follow-up to expect: “You said customisation takes about a second. Why is traffic freshness two minutes rather than two seconds?” The answer is that customisation is the smallest term in the sum. The probe upload batches, the matcher's window, and the aggregation interval that gives a segment enough observations to be trustworthy all come first, and the last of those is the one with a real floor — you cannot average over vehicles that have not driven past yet. Saying “the algorithm is not the bottleneck, the statistics are” is the answer that distinguishes someone who has designed this loop from someone who has read about the algorithm.

05b

The directions query: grammar, knobs, and a freshness contract

A routing API is a query engine, not a CRUD surface, and the useful way to think about its design is the same way you would think about a search API: what can the caller express, what can they tune about the ranking, and what are they promised about how current the answer is. That last one is unusual and it is where most designs are weakest — a route is a prediction, and a prediction with no stated vintage cannot be reasoned about by the caller.

The request

Endpoints are GET for cacheability at the edge where it helps, but the directions call is a POST because a multi-waypoint request does not fit comfortably in a query string and, as §8 explains, it is not cacheable anyway.

POST /v1/directions

{
  "origin":      { "lat": 37.7955, "lng": -122.3937 },
  "destination": { "place_id": "ChIJ_id_of_a_known_place" },
  "waypoints":   [ { "lat": 37.8078, "lng": -122.4177 } ],
  "optimize_waypoint_order": false,

  "mode": "driving",                    // driving | walking | cycling
  "departure_time": "now",              // "now" | RFC 3339 timestamp, future only
  "traffic_model": "best_guess",        // best_guess | optimistic | pessimistic
  "alternatives": 2,                    // 0-2 beyond the primary route
  "avoid": ["tolls", "ferries"],        // tolls | ferries | highways | unpaved
  "units": "metric",
  "language": "en-GB"
}

Three of those fields carry more design weight than the rest. departure_time must accept a future timestamp, because the ETA machinery in §6 is time-dependent and a query about tomorrow morning is answered from historical profiles rather than live speeds — a system that only understands "now" has hard-coded an assumption it will later have to unpick. traffic_model is the ranking knob: it selects which quantile of the predicted travel-time distribution is reported, and exposing it is what lets a calling application be conservative about a flight connection without the routing service having to guess its risk appetite. optimize_waypoint_order is the trap — it turns a shortest-path query into a travelling-salesman problem, which is why §10b treats it as a separate, tightly-quota'd cost class.

The response

200 OK

{
  "routes": [
    {
      "summary": "I-80 W",
      "distance_m": 14820,
      "duration_s": 1140,               // free-flow / historical baseline
      "duration_in_traffic_s": 1685,    // what the user is actually told
      "confidence": 0.86,               // see freshness, below
      "polyline": "ogtcF~lfhVe@...",    // encoded, ~6 bytes per point
      "warnings": ["Toll road"],
      "legs": [
        {
          "distance_m": 14820, "duration_in_traffic_s": 1685,
          "steps": [
            { "instruction": "Head west on Bryant St",
              "distance_m": 210, "duration_s": 48,
              "maneuver": "depart", "polyline_range": [0, 12] }
          ]
        }
      ]
    }
  ],
  "traffic_as_of": "2026-09-17T14:03:11Z",
  "map_version": "2026-09-a",
  "status": "OK"
}

Both duration_s and duration_in_traffic_s are returned, and returning only the second one is a mistake worth avoiding: the difference between them is the congestion signal, and clients use it to draw the red line on the map and to decide whether a delay is worth notifying about. polyline_range indexes into the route's polyline rather than repeating geometry per step, which matters because a long route has thousands of points and duplicating them per instruction roughly doubles the payload.

The freshness contract

This is the part specific to a prediction service. traffic_as_of states when the speed data behind this answer was last updated, and confidence states how well-observed the route was — derived from how many of its edges had live speeds from enough contributors versus how many fell back to historical profiles. A route across a motorway at rush hour is densely observed; the same route at 04:00 through a rural area is not, and a caller that treats both ETAs as equally trustworthy will mislead its users.

The contract is explicitly not a guarantee of currency. It is a statement of vintage, which is the honest thing a system with §2's two-minute loop can offer. When the traffic pipeline is degraded (§10), traffic_as_of simply goes stale and confidence falls; the API does not fail, and callers who care can notice.

Errors, and what they mean

Status When Why not something else
400 INVALID_REQUEST Malformed coordinates, a past departure_time, more waypoints than allowed The caller can fix these; they are not conditions of the map
200 ZERO_RESULTS Both endpoints snapped fine, but no path exists — an island, a seasonal closure Deliberately a 200: the query was valid and was answered, and the answer is "you cannot drive there". A 404 would put an answered query into the 4xx alarms that exist for broken integrations
422 UNSNAPPABLE A coordinate has no road within the snap radius — mid-ocean, deep wilderness Distinct from ZERO_RESULTS so the client can say "move the pin" rather than "no route exists"
429 + Retry-After Per-key quota exhausted, weighted by cost class (§10b) A matrix call and a tile fetch cannot share one counter; the weights are what make the quota meaningful
503 + Retry-After No routing replica holds the requested region's map version during a rollout Genuinely transient and genuinely retryable, unlike everything above it

The rest of the surface

Endpoint Shape Level
GET /v1/tiles/{version}/{z}/{x}/{y}.mvt Immutable vector tile. Cache-Control: public, max-age=31536000, immutable; the version in the path is what makes that safe L4
POST /v1/snap-to-roads A raw GPS trace in, a matched sequence of road segments out. The map matcher of §6, exposed L5
POST /v1/distance-matrix n origins × m destinations, durations only, no geometry. Hard-capped at a few hundred cells per call. The searches are shared — one per origin and one per destination, not one per pair — but the merge and the response both grow with the product, and that is what the cap bounds L6
POST /v1/isochrone "Everywhere reachable in 20 minutes" — a single-source search with no target, so it cannot use the overlay's bidirectional pruning and costs far more than a route L7
⚠

The last two rows are where an interviewer probes cost asymmetry. A directions call is one bidirectional search; a 25 × 25 matrix is 625 destination pairs, and an isochrone is a search that cannot stop early because it has no destination to meet. If all three decrement the same quota counter by one, the cheapest way to exhaust a routing fleet is to ask it polite, well-formed questions.

What mode and avoid cost the design ›

These two fields look like query parameters and are really a capacity decision. A routing profile is a metric — an array of weights over the shared topology — so n profiles mean n customisation runs and n weight arrays to distribute. On §9's numbers that is 1,250 core-seconds and roughly 490 MB of overlay weights each, every minute. Three driving-style profiles are affordable; thirty are not, which is why the API exposes a small fixed set rather than arbitrary cost functions.

Walking and cycling are worse than a multiplier, because they are not the same graph. Footpaths, stairs and cycleways are edges the driving graph does not contain, and one-way restrictions mostly do not apply. The usual resolution is one set of topology arrays carrying an access bitmask per edge, so each profile is a mask plus a weight array rather than a separate graph, and §7's offsets and edge_head stay shared. Pedestrian routing also has no live-traffic input, so it needs no customisation loop at all — its metric is rebuilt per map release, not per minute.

avoid deliberately does not get its own metric. Four flags are sixteen combinations, most of them rare, and precomputing sixteen overlays to serve a long tail is the wrong trade. Instead the flags are edge filters applied inside the search, which costs a branch per relaxation and forbids using a precomputed overlay edge whose interior path crosses a tolled segment. That last part is the honest catch: an avoidance flag partially defeats the preprocessing, so those queries are slower, and they are a separate cost class in §10b for exactly that reason.

06

A route request, end to end, and where the 200 ms goes

The dominant path is a user tapping Directions. What makes it worth tracing step by step is that the part everyone talks about — the graph search — turns out to be the cheapest thing in it, and the parts nobody mentions in interviews are where the budget actually goes.

A latency budget bar showing that of a 159 millisecond p95 route request, about 120 milliseconds is mobile network time and only 39 milliseconds is server work, with that server portion expanded below into gateway, snapping, search, unpacking, ETA evaluation, alternatives and instruction generation The whole request, p95 — 159 ms against a 200 ms budget mobile network round trip — 120 ms all server work — 39 ms expanded below The 39 ms of server work, at scale gateway 6 ms snap O/D 5 ms search 2 ms unpack 3 ms time-dependent ETA 8 ms two alternatives 9 ms instructions 6 ms The graph search from §5 is the narrowest block on this row. Turning the answer into something a person can drive costs nine times more than finding it. 41 ms of headroom against the 200 ms NFR — spent on retries, a slow radio, or a third alternative
Figure 4 — Latency budget for a p95 directions request. Server work is a quarter of the total and the graph search is a twentieth of the server work.
ℹ

Reading the budget. The 200 ms target from §2 decomposes into 39 ms of server work and about 120 ms of mobile round trip at p95, leaving 41 ms of headroom. Three consequences follow. First, routing clusters must be regional — a cross-region round trip is 70–100 ms on its own (§ Numbers to know) and would consume the entire headroom and more. Second, optimising the graph search further is pointless: taking the 2 ms search to well under a microsecond with a hub-labelling index (§5) would improve the user-visible p95 by about 1%, for tens of gigabytes of memory and no live traffic. Third, the alternatives block is the largest discretionary item, which is why the API in §5b makes the count a parameter rather than a constant — a navigation re-route asks for zero alternatives and gets its budget back.

The steps

Snap. Both endpoints are coordinates; the graph search needs nodes. The spatial index returns candidate road segments within a radius, and the snap picks one — ideally using the device heading, because a coordinate on a divided highway is metres from both carriageways and picking the wrong direction produces a route that begins with an infuriating U-turn. The destination is snapped differently: a caller wants the entrance to the building, not the nearest point of tarmac, which is why place ids carry an explicit access point when one is known.

Search. The overlay search from §5, run bidirectionally against the current metric. Two milliseconds.

Unpack. The path contains overlay edges that skip whole cells; each is expanded into the real road segments it stands for, producing the few hundred to few thousand edges the user will actually drive.

Evaluate the ETA. This is the step that is usually skipped in interviews and it is the second-largest block on the bar. Walk the unpacked edges in order, maintaining a running predicted clock, and for each edge look up the speed at the time you are predicted to reach it — live speed for the first few minutes of the trip, blending into the historical profile for that road at that hour and day for everything beyond. Add turn costs and signal delay, which for an urban route is a substantial fraction of the total and is invisible if you only sum edge lengths over speeds.

Alternatives. Generated by re-running the search with penalties applied to the edges of the route already found, then rejecting candidates that overlap it by more than a threshold or are more than a set fraction slower. Two alternatives cost slightly more than two extra searches because the overlap filtering is itself work, which is why the bar shows 9 ms rather than 4.

Instructions. Turning an edge sequence into "keep left to stay on I-80" requires road names, junction geometry, sign text and lane data — none of which was in the search graph (§3). It is a second pass over the path against a different dataset, and it is the reason the routing service is not quite as slim as the 4.6 GB figure suggests.

The other flow: how a traversal becomes a weight

Running continuously underneath is the loop that produced the speeds the ETA step just read. It has the same shape as the route path — a search problem wrapped in a data problem — but it never faces a user, which is what lets it be slow enough to be accurate.

A device in motion uploads a batch of positions. Each position is a coordinate with several metres of error, which in a city is enough to be ambiguous between a road, the service road beside it, and the one underneath it. Map matching resolves that ambiguity for the whole trace at once rather than point by point, and the standard formulation is a hidden Markov model: the hidden states are candidate road segments, the emission probability of a candidate falls off with its distance from the observed point, and the transition probability between consecutive candidates compares the driving distance between them against the straight-line distance between the two observations. Viterbi then finds the most likely sequence of segments.

Match each point to its nearest road independently and you get traces that teleport between parallel roads, and every one of those teleports becomes a fictitious traversal at an impossible speed. The joint formulation is what makes the transition term able to reject them.

Matched traces yield per-segment traversal times. Those are aggregated over a window, filtered for the minimum-contributor threshold of §10b, compared against the historical profile to reject implausible excursions, and written into the live-speed array of §7. Customisation picks the array up and re-weights the overlay. Total loop time: the two minutes of §2.

Why the ETA cannot be a sum of current speeds ›

Consider a 45-minute drive that ends on a road which is clear now and is reliably gridlocked from 17:30. Departing at 17:00, a router that sums current speeds predicts a clear run and is wrong by fifteen minutes — and worse, it may route the user onto that road in preference to an alternative that would have been better.

The fix is to make the weight a function of arrival time rather than a scalar, so evaluating an edge means asking "how fast is this road at 17:38?". Doing this properly inside the graph search is time-dependent routing, and it complicates the overlay considerably, because a cell's interior distances now depend on when you enter it. The pragmatic compromise used here, and widely in practice, is to search on a near-term metric and then evaluate the resulting path time-dependently, accepting that the chosen route is slightly less optimal than the reported ETA is accurate.

Tradeoff Correct ETAs with occasionally suboptimal route choice, versus a much heavier search. Naming this compromise, and the fact that it is a compromise, is an L6 signal.
Where the “within 10%” ETA target actually comes from ›

§2 commits to an ETA within 10% on 90% of trips, and nothing above produces that number — it only consumes speeds that are assumed to be right. The missing piece is a third loop. Every completed navigation session yields a matched traversal and a ground-truth arrival time, so the same map-matching pipeline that feeds live speeds also emits labelled examples: predicted versus actual, per segment and per trip.

Those records do two jobs. Aggregated by (segment, day type, 15-minute bucket) they are the historical profiles §7 stores and clusters. Regressed against trip outcomes they calibrate the constants the per-edge sum cannot see — junction delay, signal timing, the gap between the speed a segment carries and the speed this driver will actually hold. A pure sum of length ÷ speed is biased low on urban routes for precisely that reason, and the bias grows with the number of intersections rather than with distance.

Knowing that the error is measured as a distribution, sliced by trip length and by urban versus rural, is the difference between quoting the NFR and owning it.

Why matching is a whole-trace problem, with the arithmetic ›

A trace of 60 points with 5 candidate segments each has 560 possible assignments, which is not enumerable. Viterbi exploits the Markov structure to solve it in time proportional to the number of points times the square of the candidate count — 60 × 25 = 1,500 transition evaluations, each of which needs a short shortest-path query between two nearby segments. That last part is why the matcher is a routing client and not a standalone geometric service, and why §4 notes its query volume is an order of magnitude above the interactive one.

The emission term is typically a zero-mean Gaussian over the perpendicular distance from the observation to the candidate segment, with the standard deviation set from the device's reported accuracy. The transition term compares the on-road distance between consecutive candidates with the straight-line distance between the observations; when they diverge sharply the transition is implausible, and this is what suppresses the parallel-road teleports.

What a re-route does differently ›

Five-sixths of the route computations in §3 are re-routes, and they are a different query despite using the same machinery. The origin is known exactly and is already snapped, because the client has been matching itself to the route continuously. No alternatives are requested. The destination has not changed, so instruction text for the unchanged tail can be reused.

Most importantly, the majority of re-route checks conclude that the existing route is still best, and that conclusion can often be reached without a full search — by re-evaluating the current route's ETA under the new metric and only searching when it has degraded past a threshold. This is the single largest lever on the core count in §3 and it is worth naming when an interviewer asks how you would cut the fleet in half.

Tradeoff A threshold that is too generous leaves drivers in jams a better route would have avoided; too tight and the app reroutes constantly, which users experience as indecision.
07

Data model: a compiled array, not a database

The entities are unremarkable — nodes, edges, road attributes, speeds, tiles — and listing them teaches nothing. What decides the model here is the enormous gap between how often each one is read and how often it is written, so the access patterns come first.

Operation Frequency (fleet-wide) Query shape
Relax a node's outgoing edges inside a search ~1.1 × 109 / sec Sequential scan of a short contiguous run, by node id
Read one edge's current weight Same order Random index into a flat array, by edge id
Snap a coordinate to a segment ~50 K / sec (2 per fresh route) Spatial nearest-neighbour within a radius
Fetch names, lanes and signs along a path ~158 K / sec × ~500 edges Random by edge id, batched per route
Read a historical speed profile Once per edge during ETA evaluation Random by (edge id, time bucket)
Publish new live speeds Bulk, roughly once a minute Whole-array replace, never a per-row update
Serve a tile 1 M / sec, 30 K of them past the edge Exact key lookup on (version, z, x, y)
Change the road network itself Weekly, as a whole new artifact Offline build, no online write path at all

The first row is derived, not assumed: an overlay query settles on the order of three thousand nodes (§5), each node has about 2.3 outgoing edges (§3), and there are roughly 158 thousand route computations a second (§3) — call it seven thousand edge relaxations per query and a little over a billion a second across the fleet. Only the edge-weight reads it drives are in the same league; every other row in the table is two to three orders of magnitude below it.

Two observations force the entire model. First, the thing that is read a billion times a second is never written online. Road topology changes on a weekly release cadence, so it can be compiled into a form optimised purely for reading — no indirection, no locks, no query planner, no rows. Second, the weights on that structure change every minute while the structure itself does not. Keeping them in the same object would mean rewriting the object to change a speed. Keeping them apart means a speed update is an array swap.

The search graph

Stored as a compressed adjacency structure: one array of per-node offsets and one array of edges sorted by tail node, so a node's outgoing edges are a contiguous slice. This is the layout the 8-bytes-per-edge and 4-bytes-per-node constants in §3 describe.

// Topology — immutable for the life of a map release, memory-mapped read-only
offsets   : uint32[num_nodes + 1]   // edges of node v are [offsets[v], offsets[v+1])
edge_head : uint32[num_edges]       // the node each edge points at

// Metric — one array per routing profile, hot-swapped by customisation
overlay_w : uint32[num_overlay_edges]   // what customisation computes and publishes
weight    : uint32[num_edges]           // decisec; derived on the replica from live_kph
                                        // and the profile. UINT32_MAX = impassable

// Live speeds — the only thing the traffic loop writes
live_kph  : uint8[num_edges]        // 0 = no live data, fall back to the profile

A closure is UINT32_MAX rather than a removed edge, which is what lets §2's minutes-not-weeks requirement ride the fast path: reopening the road is another array write, and no topology was ever touched.

live_kph at one byte per edge is 495 MB for the whole planet at §3's defaults — small enough to hold densely rather than as a sparse map, even though only a minority of edges ever carry live data. The dense form is worth its extra memory because it makes a lookup a single indexed read with no branch, on the hottest path in the system, and because publishing a new one is a pointer swap that readers cannot observe half-applied.

Why not a graph database ›

Graph databases are built for a workload this is not: traversals of a few hops, expressed as queries, over data that is being modified concurrently. Here the traversal is millions of hops, expressed as a tight loop, over data that never changes while it is being read. Every feature that makes a graph database general — transactions, a query language, concurrency control, pointer chasing between records — is overhead against that loop, and the measured difference is not a few per cent but a few orders of magnitude.

The place a database does belong is upstream. The source-of-truth map data — imports, editorial corrections, closure reports, place records — lives in ordinary transactional storage, and the build pipeline of §4 compiles from it. The serving path just never touches it.

Tradeoff No ad-hoc querying of the serving graph, and any change requires a rebuild. Both are acceptable precisely because §2 put topology on a weekly clock.
Alternatives Neo4j or similar for the serving graph PostGIS with pgRouting
Historical speed profiles, and how big they get ›

Every edge needs an expected speed for any (day type, time of day), because that is what the ETA evaluation in §6 reads for the part of the trip beyond the live horizon. Stored naively — one byte per edge per 15-minute bucket per day of week — that is 495 M edges × 96 buckets × 7 days = 333 GB, which is two orders of magnitude larger than the graph it annotates.

It compresses hard, because the overwhelming majority of roads have no meaningful daily pattern at all. A residential street is at its free-flow speed essentially always. So profiles are clustered: a few thousand archetype curves are stored in full, and each edge holds a two-byte reference to its nearest archetype, with only genuinely distinctive edges keeping a private curve. That takes the common case to about 1 GB and leaves the resolution where it matters.

Tradeoff A clustered profile is an approximation, and for an edge assigned to a poorly-fitting archetype the ETA is worse. The fitting error is measurable per edge at build time, which makes "keep a private curve" a decision the pipeline can make from data rather than a guess.
Where turn restrictions live, and why they are not edges ›

"No left turn from Elm into Oak between 07:00 and 09:00" is a constraint on a pair of edges, and a plain weighted graph has nowhere to put it — the cost of traversing Oak depends on how you arrived, which the model does not represent.

Two standard fixes exist. The edge-based graph transforms the problem by making each road segment a node and each legal turn an edge, which represents restrictions natively at the cost of a substantially larger graph. The alternative keeps the node-based graph and carries a separate table of banned or penalised (incoming edge, outgoing edge) pairs, consulted during relaxation. The table is small — restrictions are rare — and it keeps the hot arrays compact, which is why it is the choice here.

Tradeoff A lookup on every relaxation, mitigated by a per-node bitmap marking the few nodes that have any restriction at all, so the common case is one bit test. Candidates who never mention turn restrictions are usually the same ones whose design cannot express them at all.
Alternatives Full edge-based graph expansion Restriction-aware shortcut weights baked at build time
The tile corpus as an object store, not a table ›

Tiles are keyed exactly, never scanned, never joined, written once and read forever — the definition of an object-store workload. The key is {version}/{z}/{x}/{y}, which doubles as the URL path from §5b and is what makes the edge cache's 97% hit rate possible without any invalidation protocol.

One detail that matters at this scale: a hundred million small objects is hard on both object stores and filesystems. The usual answer is to pack tiles into a small number of large files with an index — the archive formats built for exactly this — so the origin does a seek inside a big blob instead of opening one of a hundred million files.

08

The read path: what caches perfectly, and what cannot cache at all

Most systems have one caching story. This one has two, pointing in opposite directions, and the interesting half is the side where caching does not work — because the resolution there is what most of §5 was about.

Tiles: about as cacheable as anything gets

A tile's content depends on nothing but its coordinates and the map version. Every user looking at the same square of the world at the same zoom gets byte-identical data, the version is in the URL, and the object is therefore immutable forever. That is half of §3's 97% edge hit rate. The other half is demand: a metro's working set is a few thousand tiles that everyone there requests, and immutability is what lets the edge keep them. Over a uniform draw from 107 million objects the same immutable keys would miss almost every time — which is exactly the argument that kills the route cache below.

Layer What it holds TTL Hit rate
Client disk cache Tiles for recently-viewed areas, plus the route being navigated Until evicted; versioned so never stale High for commuters, who look at the same places daily
CDN edge Everything popular in that metro One year, immutable ~97%
Origin shield Absorbs edge misses before they fan out to the store One year Most of the remaining 3%
Tile origin The full corpus, ~8 TB (§3) n/a — it is the source n/a

Two design choices do the heavy lifting. The corpus stops at zoom 14 and clients overzoom: because vector tiles carry geometry rather than pixels, a client showing zoom 18 renders from the zoom-14 tile it already has, scaled up. Without that, the pyramid would need zoom levels 15 through 22, and since each level has four times the tiles of the one below, a pyramid to zoom 22 is on the order of 1013 tiles. Vector tiles do not make the map prettier; they make the corpus finite.

And because the version is in the path, a map release never invalidates anything. The new version populates the edge gradually as users request it, the old version stays valid and servable throughout, and a rollback is pointing clients back at the previous version string. An in-place tile update would instead invalidate tens of millions of objects at once and send the miss rate — and therefore origin load — through the roof for hours.

Routes: a response cache would be pointless

Caching route responses fails on the key space, and the arithmetic is quick. Even after snapping, an origin-destination pair is drawn from 150 million nodes, so there are on the order of 1016 possible pairs. Add departure time, travel mode and avoidance options as part of the key, and add the fact that the answer legitimately changes every minute as traffic moves, and the achievable hit rate is indistinguishable from zero. A cache in front of the directions API would consume memory to serve stale answers to nobody.

✓

The reframing: the cache in a routing system is not a response cache — it is the precomputed overlay from §5. Those cell weights are exactly the reusable intermediate result that a response cache was reaching for, and they work where a response cache cannot because they are keyed by region rather than by query. Every route crossing a cell reuses that cell's weights, which is a hit rate near 100% against a key space of a few hundred thousand cells rather than 1016 pairs. Customisation is cache population; the two-minute freshness bound of §2 is the TTL.

Smaller things around the edges do cache conventionally, and they are worth naming because together they account for a meaningful slice of the §6 budget:

Cached Key Why it works
Snap results Coordinate rounded to ~10 m, plus heading bucket People start journeys from a small number of places — homes, stations, car parks — so rounding collapses a continuous space into a repeated one
Geocoded place ids Place id Place records change on a human timescale, not a traffic one
Instruction text per edge (edge id, manoeuvre, language) The same junction generates the same phrase for every driver; a genuine hit rate on the 6 ms instruction block in §6
Unpacked overlay paths Overlay edge id, per metric version Popular corridors are traversed by many routes, so unpacking the same cell crossing repeatedly is avoidable — at the cost of invalidating on every customisation

The last row is the one with a real tradeoff rather than a free win: it is a cache whose contents are invalidated wholesale every minute by customisation, so its value depends entirely on whether a corridor is hot enough to be re-requested many times inside one traffic window. In dense metros it is; in rural regions it is not, and it is reasonable to enable it per region rather than globally.

⚠

The client is the most important cache in this system. A navigating device holds the entire route geometry, the instruction list, and the tiles along the corridor. That is why a driver keeps getting turn prompts through a tunnel with no signal, and it is why the §3 estimator counts one route computation per 30 seconds of navigation rather than a continuous stream. Designs that stream turn instructions from the server rather than shipping them once get both the connectivity story and the capacity story wrong.

Downloaded regions: the one place contraction hierarchies win ›

A downloaded region is the extreme end of the client cache: no network, so the device must hold the graph and run the search itself. That inverts the decision §5 made. Rung 4 was rejected because the node ordering and every shortcut weight are functions of the metric, and the metric changes every minute — but an offline region has no live traffic by definition. Its metric is static free-flow time, so contraction hierarchies are exactly right there: build the index once at download time and get queries in a fraction of a millisecond on a phone CPU, with no customisation pipeline to run.

What ships is a bounded sub-graph plus that index, and the numbers stay small because the area does: a metropolitan region is a few hundred thousand nodes, tens of megabytes with its tiles. The design cost is honesty about degradation. An offline region cannot learn about a closure, so it carries an expiry measured in weeks and the client must say so; ETAs come from the free-flow metric and are systematically optimistic at rush hour; the confidence field from §5b is unavailable rather than low. Saying which guarantees lapse is the answer here — claiming the offline experience is the same one is not.

09

Scaling: getting a new metric to every replica, every minute

§3 established that routing compute is small — a few hundred cores for the planet — and §4 established that the graph is replicated rather than sharded. Between them they dispose of the scaling question most candidates prepare for. Adding routing capacity is adding identical machines, there is no hot shard, and there is no rebalancing.

The scaling problem that actually exists is distribution: getting a freshly customised metric onto every replica in every region, once a minute, without saturating anything. That is the deep dive.

Sizing the overlay, and therefore the update

Work it through from §5's structure. At three thousand nodes per cell, a 150-million-node graph has about fifty thousand cells. A cell of that size in a near-planar graph has on the order of fifty boundary nodes, and the overlay holds one edge per ordered pair of them — about 2,450 per cell, so roughly 122 million overlay edges in total. That is reassuringly close to the one-shortcut-per-node constant §3 used for the memory estimate, which is a useful consistency check to perform out loud.

The same decomposition gives the customisation cost. Each cell needs one search per boundary node, confined to that cell's three thousand interior nodes — call it half a millisecond each, so 25 ms per cell, so about 1,250 core-seconds for the planet. On 1,250 cores that is the one second quoted in §5, and the number of cores is a dial rather than a constraint because cells are perfectly independent.

At four bytes per weight, a full overlay metric is roughly 490 MB. Publishing that to every routing replica every minute is where the design has to be careful.

Customisation runs per region and publishes only the cells whose weights changed, roughly 75 megabytes a minute, through a distribution tree to regional routing replicas that apply it as an atomic pointer swap Speed aggregator new live_kph, ~1/min Customisation, cell-parallel 50 K cells × 25 ms = 1,250 core-s Validation gate canonical routes re-checked publish only changed cells — ~15% — ~75 MB Distribution tree fan-out, not N pulls from one publisher Region: europe-west replica replica apply = atomic pointer swap us-east replica ×N holds the whole planet Region: asia-south replica replica old metric stays readable until swap Replicas are interchangeable; the only thing that differs between them is which metric version they have applied.
Figure 5 — The metric distribution path. The routing fleet scales by adding identical machines; the pipeline that keeps them current is what needs designing.
Publishing deltas, and what happens when a replica misses one ›

A full overlay metric is ~490 MB. Sending that to every replica every 60 seconds is 8 MB/s per replica sustained, which is survivable per machine and ugly in aggregate. It is also wasteful, because most of it did not change: only cells containing an edge whose live speed moved need recomputing, and globally that is perhaps 15% in a given minute — dense metros during rush hour, very little elsewhere at 03:00.

So the publish is a delta keyed by cell, roughly 75 MB, and each delta names the metric version it builds on. A replica that misses one detects the version gap and requests a full snapshot rather than applying a delta to the wrong base. That fallback has to exist and has to be rate-limited, because the failure mode where every replica simultaneously decides it needs a full 490 MB snapshot is how a small blip becomes an outage.

Tradeoff Deltas add a versioning protocol and a reconciliation path in exchange for roughly 6× less traffic. Full snapshots every round are simpler and are the right choice at a smaller scale.
Applying a new metric without pausing queries ›

A replica is answering a thousand queries a second while a new metric arrives. Mutating the weight array in place would let a single search read some edges at the old speed and some at the new — not catastrophic, since both are plausible values, but it makes results irreproducible and it is avoidable for free.

Instead the new arrays are assembled alongside the old and a single pointer is swapped. Searches in flight keep the array they started with; searches that begin after the swap get the new one. The old arrays are freed once the last in-flight search holding them finishes. The cost is transient double memory for the changed portion, which at 75 MB against 4.6 GB is negligible.

Tradeoff Brief extra memory in exchange for every query seeing one internally consistent view of the world. Note this is a statement about a single replica: two replicas can be a metric version apart, so two identical requests seconds apart can legitimately differ.
Rolling out a new map version across a multi-gigabyte fleet ›

A map release changes topology, so it changes the graph, the overlay structure, the tile corpus and the edge ids that everything else refers to. A replica cannot apply it incrementally; it loads several gigabytes and restarts into the new artifact.

That makes the rollout staged by construction: a canary fraction of replicas per region, then widening over hours, with old and new coexisting throughout. Two consequences are worth stating because they are where this bites. Edge ids are not stable across versions, so a metric customised for version n is meaningless to a replica on version n+1 — the customisation pipeline must produce one metric per live map version during the overlap window. And a client session that spans the rollout can get a route from a new replica and instruction text from an old one, which is the mixed-version failure in §10 and is why the response in §5b carries map_version.

Tradeoff Hours-long rollouts and a period of doubled customisation cost, in exchange for a release that can be halted and rolled back at any point.
The one part of this system that does shard: the probe pipeline ›

Routing cannot be partitioned geographically because a query does not know which regions it needs (§4). Map matching is the opposite: a trace is a local object, every candidate segment for it is within metres, and nothing about matching one trace depends on any other. So the probe stream partitions cleanly by geographic cell, and the matcher scales horizontally in the ordinary way.

Draw that contrast in an interview: it shows the partitioning decision being made from the access pattern rather than from habit. The same data, the same geography, two opposite answers, both correct.

The one wrinkle is traces that cross a partition boundary. Assigning a trace to the partition of its first point and letting the matcher read a margin of graph beyond its own cell handles it, at the cost of every matcher holding slightly more than its share.

Hot spots: not keys, but cities ›

There is no hot-key problem in the usual sense — every replica holds everything, so no single machine owns a popular road. What does concentrate is load by geography: a stadium emptying after a match puts tens of thousands of simultaneous route requests, an enormous probe spike and a burst of tile demand into a few square kilometres.

Routing absorbs it as ordinary regional load. The tile edge absorbs it well, because everyone is looking at the same tiles and the hit rate goes up rather than down. The part that strains is the probe pipeline, since a geographically partitioned matcher does have a hot partition, and the resolution is to sub-partition dense cells and to sample rather than match every trace once the observation count per segment is already far past the threshold that makes the estimate trustworthy.

Tradeoff Sampling under load costs nothing in accuracy where data is abundant, which is exactly where load is highest. It would be unacceptable in a sparse region, so the sampling rate has to be driven by local observation density rather than by a global switch.
10

Failure modes: stale weights, bad builds, and roads that are not there

This system degrades unusually well, and knowing exactly how is most of the answer. Almost every input has a previous version that remains servable, so the dangerous failures are not the ones where something stops — they are the ones where something keeps going while being wrong.

Scenario Problem Solution Level
A routing replica dies mid-request One request fails; the replica held no session state Gateway retries on another replica. Replacement takes minutes because a new process must load several GB, so capacity is provisioned for peak rather than autoscaled into it L4
Tile origin unavailable Only the 3% of requests that miss the edge are affected Nothing to do for the common case; the edge keeps serving immutable tiles indefinitely. Users panning into unvisited areas see gaps, which is the correct degradation L4
Traffic pipeline stalls Replicas keep serving the last metric, so routes silently reflect the world as it was an hour ago — and confidently Age out live speeds explicitly: past a staleness threshold, fall back to historical profiles, drop confidence, and let traffic_as_of tell callers (§5b). The failure must be visible in the response, not only on a dashboard L5
Replicas on mixed map versions during a rollout A route computed against one version paired with instruction text from another; edge ids do not agree Pin a version for the whole request and return it (§5b). A client continuing a session sends it back, and the gateway routes to a matching replica or returns 503 L5/L6
Customisation publishes a corrupt metric A bug that zeroes or inverts weights makes every route nonsense, fleet-wide, within a minute — the fastest path to a global outage in this design A validation gate between customisation and publish: re-run a fixed corpus of canonical routes and reject the metric if any distance moves beyond a plausible band. Deltas make this cheap, since only changed cells need checking L6
A closed road is not in the data Users are routed into a flooded underpass or a closed mountain pass. The only failure here with a physical safety consequence A closure fast path independent of the build pipeline: authority incident feeds and corroborated user reports write directly to the metric as an impassable weight (§7), reaching replicas in the same two minutes as congestion L6
Anomalous probe data moves a weight A handful of devices, malicious or merely stationary in traffic on a parallel service road, drag a segment's speed away from reality Minimum contributor threshold, per-device contribution caps, and a clamp on how far a segment may deviate from its historical profile in one window (§10b) L7
A map build disconnects part of the graph A bad import deletes a bridge or mis-tags a ferry, and an island or a whole region becomes unroutable. It survives release because nothing crashes Release gates on graph invariants: strongly-connected-component analysis against the previous version, plus a canonical route corpus that must still return routes of plausible length. This is the same idea as the metric gate, applied at the slower clock L7/L8
✓

The pattern across the last four rows is one idea applied at four timescales: nothing reaches the serving path without being checked against the previous version of itself. A metric is checked against canonical routes before publish, a map build against connectivity invariants before release, a probe observation against its segment's historical profile before aggregation. Each gate answers the same question — is this new value plausible given what we believed a moment ago — and each one exists because the component it guards fails silently rather than loudly.

10b

Abuse and privacy: poisoned traffic, expensive queries, and location data

The threats here are specific to a system that is publicly readable, expensive to query, and built from data its users generate by moving around. That last property is the one with no analogue in most system design questions, and it is where the strongest answers go.

Traffic data poisoning

The routing metric is computed from device-reported positions, which makes it an input an attacker controls. This is not hypothetical: in 2020 an artist walked a handcart of 99 borrowed phones, all running Google Maps navigation, slowly down empty Berlin streets, and the streets turned red — the system inferred a traffic jam from a cart. It was a stunt with no victim, and it is a clean demonstration of the attack. The version with a victim redirects traffic away from a business, or towards a road an attacker wants congested.

The defences are statistical rather than cryptographic, and they compound:

  • A minimum contributor threshold. A segment's live speed is only published if at least k distinct devices traversed it in the window, with k in the low tens. Below that, the historical profile stands. This alone raises the cost of the handcart attack by an order of magnitude.
  • Per-device contribution caps. One device contributes at most one observation per segment per window, and devices are weighted down when their traces are implausible — physically impossible accelerations, positions inconsistent with the cell tower or the reported accuracy, several devices moving in unnaturally tight formation.
  • A deviation clamp. A segment's speed may not move more than a bounded fraction from its historical profile in one window. Real congestion builds over minutes and is admitted; a step-change to a walking pace on an empty motorway is rejected and alerted on.
  • Device attestation. Raising the cost of a synthetic fleet by requiring probe uploads to come from attested app installations. This raises the bar without ever being decisive on its own — the Berlin attack used ordinary phones running the real app.

L5 asks how the handcart is stopped. L6 asks who is trusted to report a closure, and what happens when a mapping authority's feed is wrong. L7 asks how you would detect a coordinated campaign across a city rather than a single anomalous segment, which is a different problem: correlated anomalies across adjacent segments look exactly like real congestion, and the signal that separates them is the contributing device population, not the speeds.

Location privacy and retention

The probe pipeline ingests 20.7 TB a day (§3) of where people are. A sequence of positions from a single device is among the most identifying data any consumer system holds — a commute identifies a home and a workplace, and those two points identify a person — so the design question is how quickly the raw form stops existing.

  • Rotating pseudonyms. The identifier on a probe batch is not a stable device id and is rotated frequently, which limits how long a single trace can be. It is a mitigation rather than a solution: rotation does not prevent linking traces by where they start and end.
  • Aggregate early, discard the input. The output of the pipeline is a speed per segment per window, with no device attribution. Raw traces have a short retention measured in hours to days — long enough to reprocess after a matcher bug, not long enough to be a history.
  • The k-threshold does double duty. The minimum contributor count that blocks poisoning is also what makes the published aggregate non-identifying: a speed derived from one vehicle on a rural lane at 04:00 is that person's journey. One mechanism, two unrelated threats.
  • Separate the product from the pipeline. A user's own saved location history is a distinct system with distinct consent, retention and deletion semantics. Conflating it with the traffic pipeline is a compliance error and an architectural one, because deleting a user's history must not require recomputing historical traffic.

Scraping and cost-of-abuse

The map is the asset, and every API response gives away a piece of it. A patient scraper issuing valid requests can reconstruct road geometry, travel times and place data, and no request in that campaign looks anomalous on its own. Detection is behavioural — systematic spatial coverage, grid-shaped query patterns, request volume with no corresponding map rendering — and the response is graduated: quota, then degradation, rather than a block that simply teaches the scraper to spread out.

The sharper operational problem is cost asymmetry, from §5b. A tile is served from an edge cache; a directions call is a millisecond of CPU; a 25 × 25 distance matrix is fifty searches feeding a 625-cell merge; an isochrone is a search that cannot terminate early. A quota that counts requests makes the expensive calls effectively free, so quotas are weighted by cost class, and the weights are published so callers can budget rather than discover the limit by hitting it.

?

The probe that separates levels here: "Your minimum contributor threshold is twenty devices. What happens on a rural road that never sees twenty cars in a window?" The answer is that it never gets a live speed and always uses its historical profile — which is correct, because a road with no traffic has no congestion to report. The follow-up is the real question: how do you distinguish that road from one where the pipeline is broken? The distinction is not in the speeds, which look identical; it is in whether the observation count is expected to be low for that segment at that hour. Monitoring the ratio of observed to expected coverage, per segment class and time bucket, is what turns a silent failure into an alert.

11

How to answer this question at your level

The ladder in §5 is also the level ladder, which makes this question unusually legible to an interviewer. Each rung corresponds to noticing something the previous rung missed, and the gap between L5 and L6 is almost entirely the moment of realising that preprocessing and live traffic are in conflict.

L4 A map that draws and a router that routes ›
What good looks like
  • Models roads as a directed weighted graph where nodes are intersections, not a grid or a set of points
  • Picks Dijkstra or A* and can say what the edge weight is — travel time, not distance
  • Serves the map as a zoom-level tile pyramid behind a CDN, and knows tiles are cacheable because their content depends only on coordinates
  • Separates the two halves of the product rather than giving them one architecture
  • Handles one-way streets, and realises the graph must be directed to do so
What separates L5 from here
  • No sense of how slow the search actually is — treats "use Dijkstra" as the answer
  • Proposes caching route responses without checking the key space
  • Renders tiles per request, turning a storage problem into an unbounded compute one
L5 Measured the gap, and bought preprocessing to close it ›
What good looks like
  • Estimates the graph at tens to hundreds of millions of nodes from a defensible starting point, not a round number pulled from nowhere
  • States that a continental Dijkstra settles millions of nodes and takes seconds, so the gap to the budget is orders of magnitude
  • Reaches for hierarchy or contraction, and can explain what a shortcut edge is and why it preserves distances
  • Gets the graph size right and concludes it fits in memory, so the fleet replicates
  • Splits the latency budget and notices the network dominates
  • Vector tiles with client-side overzoom, and the reason: the pyramid is otherwise unbounded
What separates L6 from here
  • Designs a traffic system and a preprocessed router without noticing they are incompatible
  • ETA is a sum of current speeds, so a 45-minute trip is priced as if traffic were frozen
  • Treats GPS points as landing exactly on roads; no map matching
  • No answer for what a road closure does, or how fast it propagates
L6 Saw the conflict, and split the preprocessing ›
What good looks like
  • Names the conflict explicitly: the hierarchy and its shortcut weights are functions of the metric, and the metric changes every minute
  • Separates metric-independent structure from per-metric customisation, and can explain why customisation parallelises across cells
  • Time-dependent ETA evaluation, blending live speed near the origin into historical profiles further out
  • Map matching as a whole-trace problem, and why point-by-point nearest-road matching manufactures impossible traversals
  • Closures as an extreme weight rather than a topology change, so they ride the fast path
  • Knows re-routes dominate the load and that most of them need no search at all
  • Turn restrictions are representable, by whichever of the two standard mechanisms with the tradeoff stated
What separates L7 from here
  • No gate between a freshly computed metric and the serving fleet, so one bad customisation is a global outage in under a minute
  • Cannot say which component dominates cost, or what would be cut first
  • Treats probe data as trustworthy input
  • No position on when a location trace stops being personal data
L7/L8 Owns the freshness loop and what it costs ›
What good looks like
  • Treats the probe-to-weight loop as the product's real engine and designs it end to end, including its distribution to replicas and the delta protocol
  • Validation gates at every clock — metric against canonical routes, build against connectivity invariants, observation against historical profile
  • Names the minimum-contributor threshold as simultaneously the anti-poisoning and the anonymity mechanism
  • Knows tile rendering, not routing, is the largest line item, and that the lever is re-render scope rather than renderer speed
  • Observability that is specific: per-segment observed-versus-expected coverage, metric version skew across the fleet, customisation-to-apply latency, fraction of route edges falling back to profiles, ETA error distribution by trip length
  • Can state what degrades and in what order when each input fails, and which failure is silent
  • Treats the routing technique as a reversible decision with a stated cost, not an axiom
What an L8 adds
  • Argues the map-data sourcing strategy — licensed, surveyed, community, derived from imagery — as a cost and quality decision rather than an input
  • Positions the routing engine as a platform with external consumers, so the API contract and its cost classes become the load-bearing interface
  • Has a position on which parts are worth building versus buying at this scale, and can defend it with the numbers from §3
  • Sequences the build rather than presenting it whole: start with query-time weights and no customisation pipeline, and name the traffic volume at which the migration to precomputation pays for the operational burden it adds — and what reversing it would cost

Classic probes, and how the answers differ by level

Probe L4 L5 L6 L7/L8
"How does live traffic change the route?" Recompute the route with new weights Edge weights come from observed device speeds, refreshed periodically Notices this breaks the preprocessing; splits topology from metric so only weights are recomputed Designs the whole loop — matching, thresholds, clamps, delta distribution, a gate before publish — and names two minutes as a statistical floor, not an algorithmic one
"Why not precompute every shortest path?" Too much storage Does the arithmetic: ~1016 pairs, not storable at any price Raises hub labelling as the serious version, and rejects it for being metric-dependent rather than for being large Frames precomputation as a spectrum from none to all-pairs, and picks a point on it from the freshness requirement rather than from the latency one
"How do you know a road is congested?" Users report it Devices report speed; average per road segment Positions are ambiguous between parallel roads, so matching is a whole-trace inference problem before any averaging happens Treats the input as adversarial and under-sampled: contributor thresholds, deviation clamps, coverage monitoring, and the same threshold doing anonymity duty
"How do you shard the graph?" By region — each server owns an area Sizes the graph first, then shards by region with boundary nodes Computes it at a few gigabytes and declines to shard: a long route touches everything, so replication is strictly better Notes the probe pipeline does shard by geography, and explains why identical geography gives opposite answers — one workload knows its working set in advance and the other does not
12

Numbers to know

Latency: the numbers that set the budget

Number Value What it settles
Route query budget, p95 200 ms end to end That roughly 40 ms is available for server work once the wire is paid — the constraint every other row is measured against (§2, §6)
Mobile network round trip, p95 ~120 ms That three-quarters of user-visible latency is not yours to optimise, which is why the client is given a whole route to navigate on rather than a stream of instructions (§6, §8)
Dijkstra on a continental road graph ~2 s, ~9 M nodes settled That the shortfall is ~50× rather than a tuning problem, which is the entire justification for a preprocessing stage (§5)
Overlay query after preprocessing ~1–2 ms, a few thousand nodes That the search is the smallest block in the §6 budget, so further search optimisation buys about 1% of the user-visible number (§6)
Overlay customisation, continental ~1 s across ~1,250 cores That the two-minute freshness target is set by aggregation statistics rather than by the algorithm — the answer to the most common follow-up in §5 (§5, §9)
Round trip within one AZ ~0.2–0.3 ms That snapping can be a separate service: a hop is under 1% of the server budget, which is what makes the §4 split affordable (§4, §6)
Cross-region round trip (US↔EU) ~70–100 ms That routing clusters must be regional and hold the whole planet each — one cross-region hop alone exceeds the entire server budget (§6, §9)
Main memory read ~100 ns, against ~0.1 ms for a 4 KB NVMe random read That RAM residency is not an optimisation but a requirement: a billion edge relaxations a second is three orders of magnitude out of reach on even fast SSD (§7)
✓

The two rows that decide the design are the third and the fifth. Two seconds against a 40 ms budget is what forces preprocessing to exist at all, and one second of customisation against a two-minute freshness target is what proves the preprocessing can be made compatible with live traffic. Everything between them — the cells, the overlay, the delta protocol — is machinery in service of holding both numbers at once.

Precision is not the point. "Seconds for a naive search, milliseconds after preprocessing, and about a second to re-weight" gets to the same architecture as the exact figures, and the assumption underneath them — 150 million nodes, a 200 ms budget, two-minute freshness — is the part an interviewer will actually push on.

How the pieces connect
Every decision in this design traces back to a requirement or a capacity number.
01
200 ms route budget over a 150 M-node graph (§2, §3) → an honest Dijkstra settles millions of nodes and takes seconds, so the search cannot start from scratch at request time → the speedup structure is precomputed offline and the query only walks it (§5). Every other decision in this design is downstream of moving work out of the query.
02
Live traffic must reach routes within ~2 minutes (§2) → a speedup structure that bakes weights into itself, as contraction hierarchies do, would need a ten-minute rebuild per update → the preprocessing is split into a metric-independent part and a per-metric customisation (§5) → which is why §9's scaling story is a customisation pipeline rather than a bigger routing fleet.
03
A route from Lisbon to Warsaw touches the whole continent (§5) → no partition of the graph keeps long routes inside one shard → every routing server holds the entire graph in RAM, and the fleet replicates rather than shards (§4, §9). The 4.6 GB figure in §3 is what makes that affordable, and it is the reason routing servers are interchangeable.
04
Only ~40 ms of the 200 ms budget is server work (§6) → the wire dominates the response, not the search → routing servers sit in every region and the client is given enough route to navigate offline between requests (§6, §8). It is also why re-routes, not fresh queries, are five-sixths of the load in §3.
05
Origin-destination pairs are effectively unique (§8) → a response cache for routes has a hit rate near zero → the thing that is cached is the precomputed overlay, shared across every query that crosses a cell (§8). Tiles go the other way: 97% of tile requests never reach an origin, which is why §3 sizes the tile corpus and not a tile fleet.
06
Speeds are inferred from user devices (§6) → the input to the routing metric is attacker-controlled, and 99 phones in a handcart once invented a traffic jam → probe data is matched, aggregated behind a minimum-contributor threshold, and bounded against historical profiles before it can move a weight (§6, §10b). The same threshold that blocks poisoning is what makes the retained aggregate non-identifying.

System Design Mock Interviews

AI-powered system design practice with real-time feedback on your architecture and tradeoff reasoning.

Coming Soon

Practice Coding Interviews Now

Get instant feedback on your approach, communication, and code — powered by AI.

Start a Coding Mock Interview →
Also in this series