Design Yelp / Nearby Places
Searches outnumber business edits by thousands to one, and the whole location index fits in a few gigabytes. The design turns a radius query into a lookup of a few geohash cells, served from replicas and a cache.
1 · Requirements
The Prompt
Return businesses within a chosen radius of a user, ranked by distance and rating, for 100M daily users. Business details change rarely.
Functional
- Find businesses within a radius of a location, optionally by category
- Rank results by distance and rating, with paging
- Show business details
- Owners add and edit businesses
Non-Functional
- Nearby searches return in under 200 ms at p99
- Handles about 15,000 searches a second at peak
- Business edits can take until the next day to appear in search
2 · Estimates
Drill This Phase First
Show the Answer and Estimates
Answer
About 5,800 a second
100M times 5 is 500M searches a day. 500M divided by 86,400 seconds is about 5,800 a second, and about 15,000 at a 2.5x peak.
- Searches
- 100M daily users at 5 searches a day: 500M a day, about 5,800 a second and 15,000 at peak
- Businesses
- 200M businesses at about 1 KB each: roughly 200 GB of details in SQL
- Geo Index
- 200M entries of a geohash and an ID at 16 bytes each: about 3.2 GB, small enough for memory on every replica
- Writes
- About 100,000 business edits a day, roughly 1 a second, so writes barely register
- Cache
- Hot cells in dense cities take most of the traffic. An 80% hit rate leaves about 3,000 index queries a second at peak
- Replicas
- Each read replica serves about 1,000 queries a second, so 3 carry the misses and 5 leave room for a failure
3 · API
GET /search/nearby?lat=&lng=&radius_m=&category=&page_token= → { businesses[{ id, name, distance_m, rating }], next_page_token }
GET /businesses/:id → { name, address, hours, rating, photos[] }
POST /businesses { name, lat, lng, category, hours } → { id } (owner-authenticated)
PUT /businesses/:id { fields } → 204 (searchable after the nightly index rebuild)Data Model
businesses (SQL, primary + read replicas):
business_id (PK) : bigint
name : string
lat, lng : double
category : string
rating : decimal updated in batch from reviews
hours : json
geo_index (SQL, read replicas):
geohash : string(6)
business_id : bigint
PK (geohash, business_id)
cache (Redis):
"cell:{geohash}" → list<business_id>
"biz:{id}" → details, TTL 1h4 · High-Level Design
5 · Deep Dives
Geohash Cells and Neighbors
A geohash turns a point into a string where a longer shared prefix means a closer cell. A 6 character cell is about 1.2 km by 0.6 km, which suits a 500 m search. A user near a cell edge would miss places just across the line, so read the cell plus its 8 neighbors, then filter by true distance.
Geohash or Quadtree
A quadtree splits a region only where businesses are dense, so every leaf holds about 100 places and a rural search widens on its own. It lives in memory and has to be rebuilt when a server starts. A geohash index is an ordinary indexed column with no rebuild step, which is why it wins when the data changes slowly.
Ranking and Sparse Areas
Rank candidates by a blend of distance and rating after the distance filter, and page through dense downtown cells with a token. When a rural search finds too few results, drop one geohash character, which covers 32 times the area, and tell the user the radius was widened.
6 · Wrap Up
The Short Version
Reads dwarf writes, so index locations by geohash on read replicas and cache hot cells. Search the user's cell plus its 8 neighbors, then filter by true distance. Business edits can wait for the nightly index rebuild.