Design Street View image ingestion and storage
Cars and taxis upload terabytes of imagery a day; stitch, index by location and serve a panorama in under a second.
- Object and blob storage — Immutable files by name, replicated and cheap.
- Geospatial indexing — Geohashes, quadtrees and S2 cells: turning 'nearby' into a range query.
- Asynchronous workers — Taking slow work off the request path and reporting back later.
- Batch versus streaming (MapReduce and friends) — Exact answers over everything at night, or approximate answers now.
- Separate metadata from content — Small hot records in a database, big cold bytes in object storage.
01The design, in the six-step framework
1. Requirements
Functional
- Capture vehicles (cars, taxis, trekkers) upload raw camera frames with GPS, heading and time.
- A pipeline stitches frames into 360-degree panoramas, blurs faces and plates, and links each panorama to a road position.
- Maps users open the panorama nearest to a point and step along the road to neighbours.
- Out of scope: the vision algorithms themselves (treated as CPU-heavy black boxes), 3D reconstruction, indoor imagery.
Non-functional
- Ingest: 1,000 vehicles active, each producing 50 GB of raw frames per driving hour; uploads arrive in bursts when a vehicle reaches a depot or Wi-Fi.
- A new panorama is published within 24 hours of upload; blurring is mandatory before anything is visible.
- Serving: 100 million panorama views a day, first tiles in under 500 ms, full panorama under 1 s on a good connection.
- Raw frames are kept for years (reprocessing with better models); nothing is lost after the vehicle has acknowledged a successful upload.
1,000 vehicles × 8 h × 50 GB = 400 TB of raw frames per day ≈ 4.6 GB/s averaged, but uploads are bursty: a vehicle at a depot with a 10 Gbit/s link pushes 400 GB in about 6 minutes.
Published panoramas: a car captures a panorama every 10 m at 40 km/h, about 1,100 per hour, 9,000 per vehicle-day, 9 million per day across the fleet. At 2 MB per panorama (all zoom levels) that is 18 TB/day of serving assets, tiny next to the raw data.
Serving: 108 views/day ≈ 1,200/s average; each view pulls about 1 MB of tiles, so 1.2 GB/s, nearly all from the CDN.
2. Core entities
- Capture run: run id, vehicle id, start and end time, upload status, manifest of frame files.
- Raw frame: run id, sequence number, camera id, timestamp, GPS, IMU heading, blob id, checksum.
- Panorama: pano id, location (lat, lng, S2 cell), heading, capture time, source run, processing version, tile blob ids, published flag.
- Road link: pano id → neighbour pano ids along the road graph.
3. API
POST /runs→ vehicle registers a run and gets upload credentials;PUT /runs/{id}/frames/{seq}→ upload one frame (resumable, checksummed);POST /runs/{id}/complete→ manifest is final, processing may start.GET /panos?lat=&lng=&radius=→ nearest published panoramas with heading and capture date.GET /panos/{id}/tiles/{zoom}/{x}/{y}→ a tile; immutable, served from the CDN.GET /panos/{id}/links→ neighbours for navigation arrows.
4. High-level design
One run from the road to a user's screen:
- The vehicle records to local disks and uploads frames opportunistically; each frame is a separate resumable object with a checksum, so a lost link costs one frame.
completeseals the manifest. - The orchestrator breaks the run into stages per road segment: align frames with GPS and IMU, stitch into panoramas, detect and blur faces and plates, generate tile pyramids, snap to the road graph. Each stage is a job on a queue consumed by stateless workers.
- Tiles land in the tile store; only after blurring passes does the orchestrator write the panorama row with
published = trueinto the geo-sharded index. - A user drops a pin: the index returns the nearest panorama by S2 cell, and the client pulls tiles through the CDN, lowest zoom first.
5. Deep dives
How does ingest absorb 400 GB bursts from a depot without losing frames?
The vehicle is the first buffer: it has a day of disk, so the upload is a background sync, not a live stream. The gateway writes straight to object storage with per-frame checksums and only then acknowledges, so the vehicle may delete local copies after an ack and a run is complete only when every manifest entry is confirmed. Depot links are the bottleneck, not the storage: a 10 Gbit/s link moves 400 GB in 6 minutes, so a depot with 20 cars at once needs 2 hours or a bigger pipe, and that arithmetic is what the interviewer wants to hear. Raw frames go to the coldest storage tier immediately; they are read once by the pipeline and then perhaps once a year for reprocessing.
The pipeline is CPU-heavy and stages fail. How is it kept correct and cheap?
Treat it as a batch DAG: each stage reads immutable inputs and writes immutable outputs named by (run, segment, stage, processing version), so a retry is idempotent and a crashed worker is simply rescheduled. The orchestrator tracks stage completion in a database and never re-runs a finished stage. Cost control: blurring and stitching run on preemptible or spot machines because every job can be restarted; a priority lane exists for runs in cities where the imagery is oldest. The 24-hour target is a throughput question: 9 million panoramas a day at 30 CPU-seconds each is 2.7 × 108 CPU-seconds, about 3,100 cores continuously, so provision 5,000 to absorb bursts.
Privacy: how do we guarantee nothing unblurred is ever served?
Make it structural, not a convention. The tile store is split into an unpublished bucket (workers write here) and a published bucket the CDN reads from; the copy between them happens only in the publish step, which runs after the blur stage has recorded success with the model version used. A separate audit sampler re-detects faces on a percentage of published tiles and raises an alarm if the miss rate climbs. Takedown requests (a homeowner asks for their house to be blurred) write a region mask to the panorama record and trigger a reprocess of that panorama only; the CDN entries for its tiles are purged by URL, and since tile URLs include the processing version, the new tiles get new URLs and old cached ones expire harmlessly.
Finding the nearest panorama in milliseconds across billions
Index panoramas by S2 cell at a level where a cell is roughly 100 m across; the index is sharded by cell prefix so a city's panoramas sit together. A nearest query lists the user's cell and its eight neighbours, computes exact distances over a few dozen candidates, and prefers the newest capture. The road-link table gives the navigation arrows and is precomputed by snapping each panorama to the road graph when it is published, because computing it at query time would need the road graph in every request. The response is small (ids, headings, dates) and cached at the edge for an hour; only tiles are large, and they are immutable.
6. What a strong answer sounds like
- Separate the three regimes: bursty ingest of cold raw data, a batch DAG of idempotent stages, and a read-heavy serving tier of immutable tiles.
- Do the depot-link arithmetic and the CPU-core arithmetic; both are the kind of capacity reasoning Google asks for in this question.
- Make the privacy guarantee structural (two buckets, publish step after blur) rather than "we are careful".
- Explain why the serving index is geographic and precomputed, not a scan of the raw frame store.
- Expect follow-ups on reprocessing all imagery with a better blur model, on multi-year storage cost tiers, and on a vehicle with a bad GPS track.
- Common mistake: designing a real-time streaming pipeline for data that arrives in 400 GB chunks once a day.
02Where this question was reported
- IGotAnOffer · TPM: 'Design Street View'
- IGotAnOffer · 'build a system to capture images of every street on Earth'
- PracHub · 'street-view image ingestion and storage system', Hard, Jan 2026
- LeetCode Discuss · Street-view image collection pipeline; no-hire at L5 (search snippet)
- 1point3acres · Storage for Street View images uploaded from taxis; strong pass (search snippet, Chinese)
- 1point3acres · Same Street View theme as last-round system question (search snippet)