Design Yelp (Proximity Service)

MediumLocation-Based ServicesGeohashQuadtreeRead-HeavySearch

Problem

Design a service for finding nearby businesses. A user searches for places within a radius of their location, optionally filtered by category, rating, price, and open now, and sees results sorted by relevance or distance. Users can view a business page with photos and reviews and write reviews. Business owners can add and update their listings. The service has 200 million businesses and 100 million daily active users.

  • What search radii are supported, for example 0.5 km to 40 km?
  • How quickly must a new or edited business appear in search results?
  • Is full-text search on business names and menus needed, or only category filters?
  • How is relevance defined: distance, rating, review count, or a blend?
  • Do we need to handle very dense areas, like downtown Tokyo, differently from rural ones?
  • Are reviews moderated before they become visible?

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • Search businesses within a radius, with filters for category, rating, price, and open now.
  • Show business details, photos, and reviews.
  • Let users write reviews with a star rating and photos.
  • Let owners create and update business listings.
  • Support keyword search on business names and categories.
  • Paginate results and show them on a map view.

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • 200 million businesses at about 2 KB of core data each: roughly 400 GB, plus photos in object storage.
  • 100 million daily users making about 5 searches each: 500 million searches per day, about 6,000 per second and 20,000 at peak.
  • Search latency under 200 ms at p99.
  • Read heavy: business edits are around 100 per second, reviews a few hundred per second.
  • New and edited listings visible in search within minutes; seconds is not required.
  • Highly available for search; writes can tolerate brief delays.

Business data is stored in a relational database, and a separate geospatial search index is kept in sync from its change stream. Search requests are served by a stateless tier that converts the location and radius into a set of geohash cells, retrieves candidate businesses from those cells, filters, ranks, and paginates. Reviews are stored separately and their aggregates (average rating, count) are denormalized onto the business for ranking.

Loading diagram...

Search nearby. The client sends GET /search?lat=&lng=&radius=2000&category=coffee&openNow=true. The search service picks a geohash precision whose cell size matches the radius, computes the center cell and its eight neighbors, and queries the geo index for businesses in those cells that match the filters. It computes exact distances, drops those outside the radius, ranks by a blend of distance, rating, and review count, and returns the first page.

View a business. GET /businesses/:id reads from the business service, which is cached heavily. Photos load from the CDN, and the first page of reviews comes from the review service.

Write a review. The review service stores the review, sends it to a moderation queue, and once approved updates the business's rating aggregates. The change stream carries the new aggregates to the search index.

Update a listing. The owner edits hours or address. The business service validates and writes to the database; if the address changed it geocodes it to new coordinates. The index updater picks up the change within a minute.

Stateless service that turns a location query into ranked nearby results.

  • Go or Java service, with Elasticsearch or OpenSearch as the geo index (geo_point fields with geohash-based filtering).

API

  • GET /search?lat=&lng=&radius=&q=&category=&minRating=&price=&openNow=&cursor= returns { results: [{ id, name, distanceM, rating, reviewCount, priceLevel, isOpen }], nextCursor }.

Scalability

The index is replicated so each replica can serve queries independently; adding replicas adds throughput. Popular query shapes (same cell, same category) are cached for a minute, which helps in dense downtown areas where many users search the same thing.

Tradeoffs

Using a search engine with geo support gives text search, filters, and geo queries in one system. A pure geospatial store (for example Redis GEO) is faster for radius lookups but cannot handle text and many filters, so it would need a second system anyway.

The data structure that finds businesses near a point quickly.

  • Geohash prefixes stored as indexed fields; an alternative is an in-memory quadtree built per region.

Diagram

Loading diagram...

Data Model

  • business_geo(business_id, geohash_12, lat, lng, category_ids, rating, review_count, price_level, open_hours)

Scalability

Storing a 12-character geohash lets any shorter prefix be matched with a prefix query. If the chosen cells return too few results, the service widens to a shorter prefix. In very dense areas it narrows instead, so candidate sets stay in the low thousands.

Tradeoffs

Geohash is simple to store in any database and easy to shard by prefix. Its weakness is cell edges: two nearby points can have different prefixes, which is why the eight neighbor cells are always included. A quadtree adapts better to uneven density but must be rebuilt or carefully updated in memory, which adds operational work that geohash avoids.

Source of truth for business listings.

  • PostgreSQL with read replicas, and a cache (Redis) for business detail pages.

API

  • GET /businesses/:id returns full details.
  • POST /businesses and PATCH /businesses/:id for owners, with authorization by ownership.

Data Model

  • businesses(id, name, address, lat, lng, phone, category_ids, price_level, hours_json, owner_id, rating_avg, rating_count, updated_at)

Scalability

400 GB fits on a single large primary with replicas. Detail page reads are served from cache; writes are low volume. If growth requires it, shard by business id.

Tradeoffs

A relational database provides constraints and transactional owner edits, and change data capture keeps the search index in sync without dual writes from application code. Dual writes from the service would risk the database and index drifting apart after partial failures.

Stores reviews and maintains rating aggregates.

  • Cassandra or DynamoDB for reviews, partitioned by business id; object storage plus CDN for photos.

API

  • GET /businesses/:id/reviews?sort=recent|helpful&cursor=
  • POST /businesses/:id/reviews with { rating, text, photoIds }.

Data Model

  • reviews(business_id, created_at, review_id, user_id, rating, text, photo_ids, status)
  • reviews_by_user(user_id, created_at, review_id, business_id)

Scalability

Partitioning by business id keeps a business's reviews together for paging. A very popular business has at most tens of thousands of reviews, well within one partition. Aggregates are updated incrementally (sum += rating, count += 1) when a review is approved.

Tradeoffs

Denormalizing the average rating onto the business and into the search index makes ranking cheap but means the rating is a few minutes stale after a new review, which users do not notice.

Keeps the geo search index consistent with the business and review databases.

  • Debezium reading the database change log into Kafka, and a consumer that upserts index documents.

Scalability

Change volume is low (hundreds per second), so a small consumer group suffices. A full reindex from a database snapshot can run in a few hours and is used after mapping changes.

Tradeoffs

Near real-time indexing with a delay of seconds to a minute meets the freshness requirement. Synchronous indexing on every write would make owner edits slower and couple write availability to the search cluster.

Related questions