Security Notes
System Design

Design Yelp (Nearby Business Search)

4 min read 7 sections

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.

sql
-- 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 business

Search Index (Elasticsearch)

Elasticsearch has first-class geo support:

json
{
  "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

sql
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

DecisionChoiceReason
Geo searchElasticsearch geo_pointMature; handles filters + geo in one query
Business dataPostgreSQLStructured; transactions for business updates
Read cachingRedis (business details)Reads >> writes; business data rarely changes
ReviewsPostgreSQL + async rating aggregationSimple; eventual consistency for avg_rating is fine
Search freshnessNear-real-time sync to ESNew businesses visible within ~60s

Security Considerations

ThreatMitigation
Review spam / fake reviewsAccount age check; purchase verification; ML spam classifier; rate limit reviews per user
Business listing manipulationVerified owner claim flow (postcard verification or phone); audit log of changes
Location spoofing in searchValidate user location (device GPS signal, IP cross-check); rate limit unusual location jumps
Photo upload abuseFile type + size validation; virus scan; NSFW classifier before publishing
PII in reviewsScan for PII (phone numbers, emails) in review text; mask or reject

Interview Tips

The geo search indexing question is the crux

discuss geohash, quadtree, and PostGIS, then say Elasticsearch with geo_point is the production-proven choice at scale.

Reads >> writes

caching strategy is important. Business detail pages change rarely; cache them aggressively.

Review aggregation

don't recompute avg_rating on every read. Store a pre-computed value and update it asynchronously.