Design a Local Business & Review Platform Like Yelp
A local business discovery and review platform (e.g., Yelp, Google Maps, TripAdvisor) allows users to search for nearby restaurants, shops, and services based on proximity, rating, price, and category. Users can read reviews, view business details, and post ratings and photos. The system requires fast geospatial proximity queries combined with full-text filters and real-time review aggregations.
1. Understanding the Problem
Functional Requirements
- Search Businesses: Search for businesses by location (current GPS coordinates or named city/neighborhood) and keyword/category (e.g.,
"Italian restaurants","open now"). - View Business Details: Display business profile, hours, photos, address, and overall aggregated rating.
- Leave Reviews & Ratings: Authenticated users can write a review, upload photos, and assign a 1 to 5 star rating (strictly one review per user per business).
- Real-Time Average Rating Rollup: Business average rating and total review count must update promptly upon new review submissions.
- Business Management: Business owners can create and update business listings, operational hours, and menus.
Non-Functional Requirements
- Low Search Latency: Proximity search queries must return results in
< 100ms(P95). - Read-Heavy Scale: 100:1 read-to-write ratio (millions of searches and profile views for every review posted).
- High Availability:
99.99%availability for searches and business profile pages. - Review Integrity & Spam Defense: Defend against fake review rings, review bombing, and sybil attacks.
Capacity Estimations & Sizing
- Total Businesses Listed: 200 Million businesses worldwide.
- Total Reviews: 2 Billion reviews.
- Daily Active Users (DAU): 50 Million users.
- Search QPS: 50,000 searches/sec at peak.
- Review Submission QPS: ~50 reviews/sec average (peaking at 200 writes/sec).
- Storage Calculation (5 Years):
- Business record:
business_id(16 bytes) +name(100 bytes) +location(16 bytes) +details(500 bytes) 700 bytes. - 200 Million businesses 700 bytes 140 GB (fits easily in database storage and memory cache).
- Reviews: 2 Billion reviews 1 KB text 2 TB storage.
- Photos: 500 Million photos 500 KB 250 TB on Amazon S3.
- Business record:
2. The Set Up
Defining the Core Entities
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BUSINESS β
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ€
β business_id β UUID β PRIMARY KEY β
β name β VARCHAR(255) β NOT NULL β
β address β VARCHAR(255) β NOT NULL β
β location β GEOMETRY β Point (Lat, Lng) β
β geohash β VARCHAR(12) β Spatial Index β
β category β VARCHAR(64) β RESTAURANT, BAR, ... β
β price_tier β SMALLINT β 1 to 4 ($, $$, ...) β
β average_rating β DECIMAL(3,2) β Pre-aggregated β
β review_count β INT β Pre-aggregated β
β opening_hours β JSONB β Operational schedule β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REVIEW β
ββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββββββ€
β review_id β UUID β PRIMARY KEY β
β business_id β UUID β COMPOSITE UNIQUE, FK β
β user_id β UUID β COMPOSITE UNIQUE, FK β
β rating_stars β SMALLINT β 1 to 5 β
β review_text β TEXT β UTF-8 β
β photo_urls β JSONB/ARRAY β S3 links β
β created_at β TIMESTAMP β NOT NULL β
β updated_at β TIMESTAMP β NOT NULL β
ββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββββββ
The API Design
1. Search Businesses by Location & Filtersβ
GET /api/v1/businesses/search?latitude=37.7749&longitude=-122.4194&radius_km=5&category=coffee&open_now=true&sort_by=rating
Response (200 OK):
{
"total_results": 142,
"businesses": [
{
"business_id": "biz_88102-991",
"name": "Sightglass Coffee",
"distance_km": 1.2,
"average_rating": 4.65,
"review_count": 1842,
"price_tier": 2,
"is_open_now": true,
"photo_url": "https://cdn.yelp.com/biz/sightglass.jpg"
}
]
}
2. Submit or Update a Reviewβ
POST /api/v1/businesses/{business_id}/reviews
Content-Type: application/json
Authorization: Bearer <jwt_token>
{
"rating_stars": 5,
"review_text": "Incredible pour-over and airy atmosphere. Highly recommended!"
}
Response (201 Created or 200 OK):
{
"review_id": "rev_44109",
"status": "PUBLISHED",
"business_new_average": 4.65,
"business_new_count": 1843
}
3. High-Level Design
Walkthrough of Core Workflows
1. Proximity Search Execution (Read Path)β
- User searches for coffee near their current GPS coordinate
(37.7749, -122.4194). - The Search Gateway converts the coordinate into a Geohash (e.g. 6-character precision, ~1.2 km radius) or Google S2 cell.
- Queries the Search Engine (Elasticsearch / Redis Geo):
- Evaluates spatial distance:
ST_DWithin(location, point, 5000). - Filters boolean criteria:
category == "coffee"ANDis_open == true. - Ranks candidate businesses based on a composite score:
Score = w1 * average_rating + w2 * (1 / distance) + w3 * review_count.
- Evaluates spatial distance:
- Returns top 20 businesses in
< 50ms.
2. Review Submission & Real-Time Rating Rollup (Write Path)β
- User writes a review
POST /api/v1/businesses/{id}/reviews. - The Review Service persists the review in PostgreSQL.
- Enforces the unique constraint:
UNIQUE (business_id, user_id)(if user previously reviewed, updates the existing row).
- Enforces the unique constraint:
- Executes atomic rating counter increment or fires a
ReviewSubmittedEventto Kafka. - Rating Aggregator Worker:
- Calculates the new average: .
- Updates
average_ratingandreview_counton theBUSINESStable in PostgreSQL and updates the Elasticsearch search index. - Primes the Redis Business Profile Cache.
4. Potential Deep Dives & Bottlenecks
Deep Dive 1: Efficient Average Rating Rollup (Avoiding Table Scans)
If a popular hotel has 50,000 reviews, running SELECT AVG(rating_stars) FROM reviews WHERE business_id = ? on every review submission or search query will thrash database CPU and disk I/O!
- Solution: Denormalized Pre-Aggregated Counters:
- We store
total_rating_sumandreview_countdirectly on theBUSINESStable. - When a new review is submitted:
UPDATE businessSET total_rating_sum = total_rating_sum + :new_stars,review_count = review_count + 1,average_rating = (total_rating_sum + :new_stars)::decimal / (review_count + 1)WHERE business_id = :biz_id;
- Handling Updates: If an existing review is edited (e.g. changed from 2 stars to 5 stars):
.
total_rating_sum = total_rating_sum + 3, whilereview_countremains unchanged. - Runtime Complexity: single-row update; eliminates table scans entirely.
- We store
Deep Dive 2: Enforcing Strictly One Review Per User Per Business
How do we ensure a user cannot submit multiple reviews for the same business, even if they click "Submit" concurrently across multiple devices?
-- Database-Level Idempotency & Unique Composite Constraint
CREATE TABLE reviews (
review_id UUID PRIMARY KEY,
business_id UUID NOT NULL REFERENCES business(business_id),
user_id UUID NOT NULL REFERENCES users(user_id),
rating_stars SMALLINT CHECK (rating_stars BETWEEN 1 AND 5),
review_text TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
CONSTRAINT unique_user_business_review UNIQUE (business_id, user_id)
);
- Upsert Semantics:
Guarantees physical deduplication at the storage engine level with zero duplicate review leakage.INSERT INTO reviews (review_id, business_id, user_id, rating_stars, review_text, created_at, updated_at)VALUES (:id, :biz_id, :user_id, :stars, :text, NOW(), NOW())ON CONFLICT (business_id, user_id)DO UPDATE SETrating_stars = EXCLUDED.rating_stars,review_text = EXCLUDED.review_text,updated_at = NOW();
Deep Dive 3: Searching by Named Locations ("SoHo", "Manhattan", "San Francisco")
Users often do not search by raw GPS coordinates; they type named locations: "Sushi in SoHo, New York". How does the system resolve named places?
- Geocoding & Location Normalizer:
- The query string
"Sushi in SoHo"is tokenized by a Location Entity Extractor. - Recognizes
"SoHo"as a predefined neighborhood polygon. - Resolves
"SoHo"into its bounding box / geographic polygon via an in-memory Polygon Spatial Index (R-Tree / PostGIS).
- The query string
- Elasticsearch Geospatial Filter:
- Executes a
geo_polygonorgeo_shapefilter matching businesses whose(lat, lng)points fall within the SoHo boundary polygon. - Applies the keyword query
"Sushi"against business names, menus, and review texts concurrently.
- Executes a
5. Architectural Trade-Off Matrix
| Design Area | Option A | Option B | Selected Choice & Rationale |
|---|---|---|---|
| Rating Calculation | On-demand AVG(stars) SQL query | Denormalized running counters on Business row | Denormalized Running Counters: read and write. Eliminates expensive 50,000-row aggregation queries during peak search traffic. |
| Search Engine | PostGIS SQL Queries | Elasticsearch / OpenSearch | Elasticsearch: Combines geospatial radial bounding boxes with BM25 full-text keyword search and faceted filtering (open_now, price) in a single query. |
| Cache Invalidation | Cache-Aside with TTL | Cache-Aside + CDC (Debezium) | CDC Event-Driven: When a business updates its hours or average rating, Debezium captures the WAL change and evicts the Redis cache immediately, eliminating stale business info. |
6. What is Expected at Each Level?
Mid-Level (L4 / IC4)
- Designs relational schemas for Businesses, Reviews, and Users.
- Enforces the one-review-per-user constraint using a unique database index.
- Uses Geohash or QuadTree for radius-based search.
- Understands the need to pre-compute average ratings to avoid slow
AVG()queries.
Senior (L5 / IC5)
- Details the denormalized atomic counter update math handling both inserts and rating edits.
- Combines full-text search with geospatial proximity in Elasticsearch.
- Solves named location searches using geocoding and polygon containment queries (R-Tree).
- Implements high-throughput read caching (Redis) for popular business profiles.
Staff+ (L6 / Principal)
- Designs anti-fraud and fake review detection pipelines: Graph clustering algorithms to detect coordinated review-bombing syndicates.
- Architects multi-region business search: Maintaining synchronized search clusters across global regions with localized master-replica routing.
- Details operational data pipeline resilience: How to recalculate and reconcile millions of business ratings from raw reviews during database corruption or schema migration without downtime.
