Proximity / Nearby Places Service (Yelp, Google Maps 'near me')
Problem statement
Design a service that returns businesses within a given radius of a user's location, with filters and ranking, for hundreds of millions of places and very high read traffic.
Requirements
Functional
Return places within radius R of (lat, lng), paginated.
Filters: category, open now, rating.
Business owners add/update/delete places (low write rate).
Place details page.
Non-functional
Search p99 < 100 ms.
Highly available; reads must not depend on the write path.
200M places; 5k–20k search QPS.
Eventual consistency for new places (minutes) is fine.
Back-of-the-envelope estimates
- 200M places × ~1 KB details = 200 GB (relational + cache); geo index entries ~50 B each = 10 GB — fits in memory on a few nodes.
- 20k QPS reads; writes ~10/s.
- Geohash precision 6 (~1.2 km × 0.6 km) → ~200M / (cells covering land) ≈ tens of places per urban cell, more in dense cities → query 9 neighbouring cells and filter by exact distance.
API
GET /v1/search/nearby?lat=&lng=&radius=1000&category=cafe&limit=20&cursor=
→ { places:[{id, name, distanceM, rating, lat, lng}], nextCursor }
GET /v1/places/{id}
POST/PUT/DELETE /v1/places (owner/admin)
Data model
places (place_id, name, lat, lng, category, rating, address, hours, ...)
geo_index(geohash VARCHAR(6), place_id) -- indexed on geohash; or a quadtree in memory
cache Redis: cell:{geohash} → [place_ids], place:{id} → details
High-level design
- Write path: place service writes to the places DB (primary) and updates the geo index; replicas serve reads. Writes are rare, so no fancy pipeline.
- Read path: location service computes the geohash of the query point and its 8 neighbours at the precision matching the radius (larger radius → shorter geohash), fetches place ids for those cells (cache → DB), computes exact haversine distance, filters, ranks, paginates.
- Index choices: geohash (string prefix ranges, simple, works in any DB with a B-tree); quadtree (in-memory tree that adapts to density — dense cities get deeper nodes); Google S2 / H3 cells (hierarchical, better edge handling). Geohash on a DB index is enough at this scale; quadtree in memory if you need sub-10 ms.
- Caching: cell → ids in Redis with TTL; place details cached separately so a place update invalidates one key.
- Ranking: distance, rating, popularity; can be a small model.
Architecture diagram
Deep dives
Why 9 cells: a point near a cell edge has neighbours in adjacent cells; querying the 3×3 block guarantees coverage for radius ≤ cell size. For larger radii, drop to a shorter geohash.
Precision vs density: fixed geohash precision gives huge cells in Manhattan and empty ones in the desert; a quadtree subdivides until each leaf holds ≤ 100 places. Trade-off: quadtree must be rebuilt/kept in memory.
Pagination: cursor = (score, place_id) after ranking; recompute per page (cheap) rather than storing result sets.
Bottlenecks & mitigations
Dense-city cells with thousands of places → deeper precision (7) in dense areas, or quadtree.
Cache stampede on popular cells (airport, stadium) → request coalescing, short jittered TTLs.
Read replica lag after a place update → owner sees own write via primary read; others eventually.
Failure modes
- Redis down: replicas serve cell queries directly (indexed), higher latency but available.
- Replica lag spike: acceptable for this workload; alert if > minutes.
- Bad data (place at 0,0): validation at write; geo bounds checks.
Follow-up questions interviewers ask
How would you support 'along my route' search?
How would you handle moving objects (drivers) vs static places?
How would you shard the index globally (by region)?