Google Maps uses a tile-based map rendering system, a graph of road segments for routing, and real-time data pipelines for traffic. The routing engine runs Dijkstra/A* on a precomputed contraction hierarchy to return routes in milliseconds.
✦ Definition~90s read
What is Design Google Maps?
Google Maps is a distributed system that ingests map data, geocodes addresses, computes routes, and serves real-time traffic overlays to billions of users. It combines graph databases, spatial indexing, and stream processing.
★
Think of Google Maps as a giant puzzle where every road is a puzzle piece.
Plain-English First
Think of Google Maps as a giant puzzle where every road is a puzzle piece. The pieces are stored in a massive grid (tiles). When you ask for directions, the system builds a custom path by connecting pieces, but it has already pre-solved the most common connections (contraction hierarchies) so it can give you an answer instantly. Traffic data comes from a separate system that watches how fast the pieces are moving.
Here's what everyone gets wrong: Google Maps doesn't compute your route from scratch. That would take seconds, not milliseconds. The secret is a precomputed hierarchy that shortcuts the entire graph. If you think it's just Dijkstra on OpenStreetMap data, you're missing the entire production playbook. The real challenge isn't the algorithm — it's the data. 20 petabytes of map data, updated in real-time from satellites, street view cars, and user reports. One stale road closure and you reroute an entire city into gridlock. By the end of this, you'll understand the architecture that serves 1 billion monthly active users with sub-100ms routing queries. You'll know how they handle traffic spikes during earthquakes, how they version map data without downtime, and why your GPS sometimes thinks you're in a river.
Why Tiles? The Map as a Distributed Cache
Before tiles, maps were monolithic images. Every zoom level required a new image. Google Maps popularized the tile grid: each tile is a 256x256 PNG at a specific zoom level. The key insight: tiles are immutable and cacheable. A tile for zoom level 12 at coordinates (1234,567) never changes unless the underlying map data changes. This allows aggressive CDN caching. The tile server just stitches vector data into PNGs on the fly, but the real magic is the tile pyramid: precompute tiles for popular zoom levels and serve from blob storage. For zoom levels 0-10, tiles are static. For 11-20, they're generated on demand and cached. The failure mode: a tile stampede when a new map version is deployed. All CDN caches invalidate simultaneously, and the tile server gets hammered. Mitigation: staggered cache invalidation and pre-warming.
Returns PNG bytes for tile. Cache hit ratio >99% for popular tiles.
⚠ Production Trap: Tile Stampede
When you deploy a new map version, all CDN caches invalidate. The tile server sees 100x traffic. Mitigate with staggered rollout and pre-warming: generate tiles for top 10% of requests before cutover.
thecodeforge.io
Design Google Maps
Geocoding: From Address to Lat/Lng in Milliseconds
Geocoding is the reverse of what most people think. You don't parse the address and look up coordinates. You tokenize the address into a hierarchy: country, state, city, street, number. Each level is a key in a sorted string table (SSTable). The trick: use a trie for prefix matching. When someone types '1600 Amphitheatre Parkway', the system returns candidates in O(k) time where k is address length. The gotcha: ambiguous addresses. 'Main Street' exists in 1000 cities. The system uses geohash of the user's IP to bias results. The failure mode: a geocoding query that matches millions of results (e.g., '1' in Manhattan). The fix: limit results to 5 and require more specificity.
Geocoder.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — SystemDesign tutorial
// Simplified geocoding with trie
classGeocoder {
privatefinalTrieNode root = newTrieNode();
publicList<GeoResult> geocode(String address) {
// Tokenize and traverse trie
String[] tokens = address.toLowerCase().split(",\\s*|\\s+");
TrieNode node = root;
for (String token : tokens) {
node = node.children.get(token);
if (node == null) returnCollections.emptyList();
}
// Return top 5 results by population bias
return node.results.stream()
.sorted(Comparator.comparingInt(r -> -r.population))
.limit(5)
.collect(Collectors.toList());
}
}
Output
List of up to 5 GeoResult objects with lat/lng and address.
💡Senior Shortcut: Geohash Bias
Use the user's IP geohash prefix to filter results. For example, if user is in geohash '9q8', only return results with geohash starting with '9q8'. This reduces ambiguity by 90%.
Routing: The Contraction Hierarchy Secret
Dijkstra on a graph with 500 million nodes is too slow. Google uses Contraction Hierarchies (CH). The idea: precompute 'shortcuts' between important nodes (highways, intersections). During query, you run bidirectional Dijkstra on the contracted graph. The result: sub-millisecond routing for long distances. The trade-off: precomputation takes hours and must be updated when roads change. The gotcha: CH doesn't handle real-time traffic well. Traffic changes edge weights, invalidating shortcuts. Solution: run CH on the static graph, then apply traffic as a post-processing penalty. This is why traffic-aware routes are slightly slower to compute.
Route object with list of edges and total travel time including traffic.
🔥Interview Gold: Why Not A*?
A with Euclidean heuristic works for small graphs but fails on road networks with obstacles (rivers, mountains). CH gives exact results and is faster for long distances. A is used for local routing within a city.
thecodeforge.io
Design Google Maps
Real-Time Traffic: The Stream Processing Pipeline
Traffic data comes from three sources: 1) GPS pings from Android phones (anonymized), 2) road sensors, 3) incident reports. The pipeline ingests millions of events per second into Kafka. A stream processor (Apache Beam) aggregates events into 5-minute windows per road segment. The output is a 'speed' value for each segment. The challenge: handling sparse data. Rural roads may have zero pings. Solution: use historical baselines and interpolate from nearby segments. The failure mode: a traffic jam causes all phones to stop moving, reducing pings. The system interprets this as 'no traffic' and clears the jam. Fix: use a minimum ping threshold; if pings drop below threshold, fall back to historical data.
If a road segment has <10 pings in a window, don't output a speed. Use historical baseline instead. Otherwise, you'll get random noise that causes phantom traffic jams.
Map Data Versioning: How to Update Without Downtime
Map data changes daily: new roads, closed roads, changed speed limits. Google uses a versioned tile system. Each tile has a version number. When new data is published, tiles are regenerated with a new version. The client requests tiles with a version parameter. If the version is stale, the server returns a redirect to the new version. This allows gradual rollout: 1% of users see new map, then 10%, etc. The gotcha: version mismatch between tiles. A user might see a new tile for one area and an old tile for another, causing visual seams. Fix: version per region, not per tile. All tiles in a region share the same version.
VersionedTile.systemdesignSYSTEMDESIGN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — SystemDesign tutorial
// Versioned tile request
// Client request: GET /tile/12/1234/567?version=20240301
// Server response:
// If version matches current: 200OK with tile
// If version is old: 302Redirect to /tile/12/1234/567?version=20240315classTileController {
@GetMapping("/tile/{z}/{x}/{y}")
publicResponseEntity<byte[]> getTile(
@PathVariableint z, @PathVariableint x, @PathVariableint y,
@RequestParam(defaultValue = "0") int version) {
int currentVersion = versionManager.getCurrentVersion(z, x, y);
if (version != currentVersion) {
returnResponseEntity.status(HttpStatus.FOUND)
.location(URI.create("/tile/" + z + "/" + x + "/" + y + "?version=" + currentVersion))
.build();
}
byte[] tile = tileService.getTile(z, x, y);
returnResponseEntity.ok(tile);
}
}
Output
HTTP 200 with tile bytes or HTTP 302 redirect to new version.
💡Senior Shortcut: Staged Rollout
Use a feature flag to control which users see the new map version. Start with 1% of users, monitor error rates, then ramp up. Rollback is instant by flipping the flag.
thecodeforge.io
Design Google Maps
Handling Traffic Spikes: The Super Bowl Problem
When a major event happens (earthquake, Super Bowl), traffic queries spike 100x. The routing service must not fall over. Solution: 1) Use a CDN for static assets (tiles). 2) For routing, use a queue with backpressure. If queue depth exceeds threshold, return cached routes for popular origins/destinations. 3) Degrade gracefully: disable traffic-aware routing during peak. The failure mode: the queue grows unbounded, causing OOM. Fix: bounded queue with rejection. Return HTTP 503 with Retry-After header. The client retries with exponential backoff.
CompletableFuture that completes with Route or throws RejectedExecutionException.
⚠ Production Trap: Unbounded Queue
Never use an unbounded queue (e.g., LinkedBlockingQueue without capacity). Under load, it grows until OOM. Always use a bounded queue with a rejection policy.
When Not to Use This Architecture
If you're building a navigation app for a single city with <100k users, you don't need CH or distributed tile servers. A single PostGIS database with pgRouting and a tile server like TileServer GL will work fine. The Google Maps architecture is overkill for small scale. The cost of maintaining a CH precomputation pipeline and a Kafka-based traffic pipeline is justified only at planetary scale. For most startups, use Mapbox or OpenStreetMap with a managed service. Don't reinvent the wheel.
🔥Senior Shortcut: Start Simple
For MVP, use Mapbox GL JS with a backend that queries OSRM for routing. That's a weekend project. Only build your own when you hit 10M daily active users.
● Production incidentPOST-MORTEMseverity: high
The 4GB Container That Kept Dying
Symptom
Routing service containers in one region were OOM-killed every 30 minutes during peak hours.
Assumption
Assumed a memory leak in the routing algorithm. Spent days profiling heap dumps.
Root cause
The graph database client had a default connection pool of 100 connections per container. Each connection cached 50MB of precomputed route data. With 4GB heap, 80 connections consumed 4GB, leaving no room for actual routing. The OOM killer struck when a burst of requests triggered connection growth.
Fix
Reduced connection pool to 20 per container. Set connection idle timeout to 60 seconds. Added circuit breaker to fail fast when pool exhausted. Memory usage dropped to 1.5GB.
Key lesson
Always profile connection pool memory overhead before tuning heap size.
A 4GB container with 100 connections is a ticking bomb.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Routing returns 'No route found' for valid locations
→
Fix
1. Check graph database connectivity. 2. Verify that both origin and destination nodes exist in the graph. 3. Ensure contraction hierarchy is built for the region. 4. Check for disconnected components due to missing edges.
Symptom · 02
Tile server returning 404s for a region
→
Fix
1. Verify tile generation pipeline completed for that region. 2. Check blob storage for missing tiles. 3. Trigger manual tile regeneration for the affected zoom levels.
Symptom · 03
Traffic data showing 0 speed for all roads
→
Fix
1. Check Kafka consumer lag for traffic pipeline. 2. Verify GPS ping ingestion rate. 3. Ensure stream processor is not stuck on a bad event. 4. Restart the stream processor if lag > 10 minutes.
★ Google Maps System Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
kubectl delete pod -l app=traffic-processor (forces restart)
Geocoding returns empty results+
Immediate action
Check geocoding service health
Commands
curl http://geocoder:8080/health
kubectl logs -l app=geocoder --tail=20
Fix now
kubectl rollout restart deployment geocoder
Feature
Google Maps
OpenStreetMap + OSRM
Routing speed
<100ms for global routes
~500ms for city-scale
Traffic data
Real-time from 1B+ devices
Requires third-party feed
Map data freshness
Daily updates
Community-driven, variable
Cost
Pay per API call
Free but self-hosted infrastructure
⚙ Quick Reference
6 commands from this guide
File
Command / Code
Purpose
TileServer.systemdesign
class TileServer {
Why Tiles? The Map as a Distributed Cache
Geocoder.systemdesign
class Geocoder {
Geocoding
Router.systemdesign
class Router {
Routing
TrafficPipeline.systemdesign
Pipeline p = Pipeline.create();
Real-Time Traffic
VersionedTile.systemdesign
class TileController {
Map Data Versioning
RoutingQueue.systemdesign
ExecutorService executor = new ThreadPoolExecutor(
Handling Traffic Spikes
Key takeaways
1
Google Maps uses Contraction Hierarchies for sub-millisecond routing, not plain Dijkstra.
2
Tile pyramids with versioning enable zero-downtime map updates and aggressive CDN caching.
3
Real-time traffic requires a stream processing pipeline with fallback to historical data for sparse roads.
4
Always use bounded queues and backpressure to handle traffic spikes gracefully.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
How does Google Maps handle routing at global scale without OOM?
Q02SENIOR
When would you choose A* over Contraction Hierarchies for routing?
Q03SENIOR
What happens when a traffic spike causes routing queue to overflow?
Q04JUNIOR
What is a tile pyramid and why is it used?
Q05SENIOR
How would you debug a sudden spike in routing latency?
Q06SENIOR
Design a system to serve real-time traffic updates to 1 billion users.
Q01 of 06SENIOR
How does Google Maps handle routing at global scale without OOM?
ANSWER
They use Contraction Hierarchies (CH) to precompute shortcuts. During query, bidirectional Dijkstra runs on the contracted graph, which has far fewer nodes. The precomputation is done offline and takes hours, but queries are sub-millisecond.
Q02 of 06SENIOR
When would you choose A* over Contraction Hierarchies for routing?
ANSWER
A is simpler and works well for small graphs (city-scale) where Euclidean heuristic is effective. CH is better for global routing because it's exact and faster for long distances. Use A for local routing within a city, CH for cross-country routes.
Q03 of 06SENIOR
What happens when a traffic spike causes routing queue to overflow?
ANSWER
The queue is bounded. When full, new requests are rejected with HTTP 503. The client retries with exponential backoff. Additionally, the system can degrade by returning cached routes for popular origin-destination pairs.
Q04 of 06JUNIOR
What is a tile pyramid and why is it used?
ANSWER
A tile pyramid precomputes map tiles at multiple zoom levels. Lower zoom levels (0-10) are static and cached aggressively. Higher zoom levels are generated on demand. This reduces server load and latency because popular tiles are served from CDN.
Q05 of 06SENIOR
How would you debug a sudden spike in routing latency?
ANSWER
First, check if it's a traffic-aware routing issue: traffic data may be delayed, causing longer computation. Second, check graph database connection pool exhaustion. Third, profile the routing algorithm for any recent code changes. Rollback if needed.
Q06 of 06SENIOR
Design a system to serve real-time traffic updates to 1 billion users.
ANSWER
Ingest GPS pings via Kafka, aggregate in 5-minute windows using Apache Beam, store results in a key-value store (e.g., Bigtable). Serve via a CDN with TTL of 1 minute. Use historical baselines for sparse data. Handle spikes with autoscaling and load shedding.
01
How does Google Maps handle routing at global scale without OOM?
SENIOR
02
When would you choose A* over Contraction Hierarchies for routing?
SENIOR
03
What happens when a traffic spike causes routing queue to overflow?
SENIOR
04
What is a tile pyramid and why is it used?
JUNIOR
05
How would you debug a sudden spike in routing latency?
SENIOR
06
Design a system to serve real-time traffic updates to 1 billion users.
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
How does Google Maps calculate route time so fast?
It uses Contraction Hierarchies, a precomputed graph that shortcuts long distances. The query runs bidirectional Dijkstra on this contracted graph, which is orders of magnitude smaller than the full road network.
Was this helpful?
02
What's the difference between Google Maps and OpenStreetMap routing?
Google Maps uses proprietary traffic data from 1B+ devices and a global Contraction Hierarchy. OpenStreetMap relies on community data and OSRM, which is slower and lacks real-time traffic.
Was this helpful?
03
How do I implement real-time traffic in my app?
Ingest GPS pings into Kafka, aggregate by road segment in 5-minute windows using Apache Beam, and store results in a key-value store. Serve via a CDN with short TTL. Use historical baselines for roads with sparse data.
Was this helpful?
04
What happens when Google Maps traffic data is wrong?
The system uses multiple sources (GPS, sensors, reports) and cross-validates. If a source is anomalous, it's downweighted. For example, if GPS pings drop suddenly, the system falls back to historical data to avoid clearing a real traffic jam.