Design Yelp (Nearby Business Search)
DifficultyMedium | HelloInterview: problem breakdown
Problem Statement
Design a service that lets users search for nearby businesses (restaurants, bars, shops) by location, category, and rating β and view detailed business pages with reviews.
πReal-world: Yelp is the canonical geospatial search problem, and its heart is "how do you index a map so 'find restaurants within 5km' is fast?" The classic answers β geohash (encode lat/long into a string prefix so nearby places share prefixes), quadtree (recursively subdivide dense areas), and Uber's H3 hexagons β all turn a 2D range query into something a normal index can serve, usually fronted by Elasticsearch for the text + facet + geo combination. Unlike Uber's moving points, Yelp's businesses are mostly static, so you can precompute heavily. The security/integrity story here is review fraud: fake reviews (paid 5-stars, competitor 1-star bombing) are an existential trust problem, and Yelp runs a famously aggressive review-filtering/recommendation algorithm plus sting operations against paid-review brokers β a real-world example of abuse detection as a core feature, which is the angle a security interviewer wants you to surface.
Requirements
Functional
- Search businesses near a location (radius or bounding box)
- Filter by category, rating, open now, price range
- Business detail page (info, hours, photos, reviews)
- User reviews and ratings
- Business owner can manage their listing
Non-Functional
- 100M businesses, 10M DAU
- Reads >> writes (search is the primary workload)
- Search latency: < 200ms P99
- Eventual consistency acceptable (new business shows up within minutes)
Core Design
The Proximity Search Problem
A naΓ―ve approach: store (lat, lng) in a DB and do WHERE ST_Distance(location, ?) < radius. At 100M businesses, this is a full table scan or requires a spatial index.
Spatial Index Approaches
Geohashencode lat/lng to a base-32 string. Nearby locations share a prefix. Index on geohash β range query on prefix. Simple, but grid cells aren't perfectly uniform.
Quadtreerecursively divide the world into quadrants. Each leaf cell holds up to N businesses. Cells in dense areas (NYC) are smaller than in sparse areas (Montana). More accurate proximity but complex to maintain.
PostGIS / MySQL Spatialbuilt-in spatial index (R-tree). ST_DWithin(location, point, radius_meters) uses the index efficiently. Great for < 10M rows; needs caching at scale.
Production choicegeohash on a fast store (Redis, Elasticsearch with geo_point) is common.
-- With PostGIS
SELECT id, name, ST_Distance(location, ST_MakePoint(-122.4, 37.7)) as dist
FROM businesses
WHERE ST_DWithin(location, ST_MakePoint(-122.4, 37.7), 5000) -- 5km radius
AND category = 'restaurant'
ORDER BY dist
LIMIT 20;Architecture
Search Query β API Gateway β Search Service
β
Search Index (Elasticsearch with geo_point)
+ Business DB Cache (Redis, 1h TTL)
β
Business DB (PostgreSQL + PostGIS)
Write (new/update business) β Business Service
β
PostgreSQL (write)
β
Sync worker β update Elasticsearch index
β
Image upload β S3 + CDN
Review Service β PostgreSQL (reviews, ratings)
β Async aggregation of avg_rating per businessSearch Index (Elasticsearch)
Elasticsearch has first-class geo support:
{
"mappings": {
"properties": {
"location": { "type": "geo_point" },
"name": { "type": "text" },
"category": { "type": "keyword" },
"rating": { "type": "float" }
}
}
}
// Query: nearby restaurants rated > 4.0
{
"query": {
"bool": {
"must": [
{ "term": { "category": "restaurant" }},
{ "range": { "rating": { "gte": 4.0 }}}
],
"filter": {
"geo_distance": {
"distance": "5km",
"location": { "lat": 37.7, "lon": -122.4 }
}
}
}
},
"sort": [{ "_geo_distance": { "location": { "lat": 37.7, "lon": -122.4 }, "order": "asc" }}]
}Reviews and Ratings
reviews(review_id, business_id, user_id, rating, text, created_at)Average rating: computed asynchronously (updated via trigger or background job when new review is submitted). Don't compute per-request β cache avg_rating on the business row.
Key Design Decisions
| Decision | Choice | Reason |
|---|---|---|
| Geo search | Elasticsearch geo_point | Mature; handles filters + geo in one query |
| Business data | PostgreSQL | Structured; transactions for business updates |
| Read caching | Redis (business details) | Reads >> writes; business data rarely changes |
| Reviews | PostgreSQL + async rating aggregation | Simple; eventual consistency for avg_rating is fine |
| Search freshness | Near-real-time sync to ES | New businesses visible within ~60s |
Security Considerations
| Threat | Mitigation |
|---|---|
| Review spam / fake reviews | Account age check; purchase verification; ML spam classifier; rate limit reviews per user |
| Business listing manipulation | Verified owner claim flow (postcard verification or phone); audit log of changes |
| Location spoofing in search | Validate user location (device GPS signal, IP cross-check); rate limit unusual location jumps |
| Photo upload abuse | File type + size validation; virus scan; NSFW classifier before publishing |
| PII in reviews | Scan for PII (phone numbers, emails) in review text; mask or reject |
Interview Tips
discuss geohash, quadtree, and PostGIS, then say Elasticsearch with geo_point is the production-proven choice at scale.
caching strategy is important. Business detail pages change rarely; cache them aggressively.
don't recompute avg_rating on every read. Store a pre-computed value and update it asynchronously.