Yelp is a platform that helps users discover local businesses such as restaurants, salons, repair shops, and more. It allows users to search for businesses, view ratings and reviews, browse photos, and see details like hours, pricing, and location.
Loading simulation...
Businesses can also claim their profiles and engage with customers through responses and promotions.
In this chapter, we will explore the high-level design of a platform like Yelp.
Let’s start by clarifying the requirements.
Before diving into the architecture, lets clearly define what we’re building.
Peak traffic may be 2-3X average load, especially during weekends, holidays, or lunch/dinner hours.
Relatively small footprint, fits well in relational or document DBs. Queryable fields (category, rating, location) should be indexed.
Stored in object storage like Amazon S3, not in databases. Backed by CDN for efficient global delivery.
The proximity service follows a microservices architecture, where each core domain (Search, Listings, Reviews, Users) is managed by an independent, stateless service.
Users and business owners interact with the system through APIs using web and mobile clients. Users can search for nearby places, submit reviews, and save favorites or bookmarks, while authenticated business owners can create and update listings.
The API Gateway acts as the single entry point for all client requests. It distributes incoming traffic across multiple service instances and handles authentication, authorization, rate limiting, and request logging.
The Search Service handles all location-based search queries, supporting filters such as distance, category, minimum rating, and open now. It queries a Geospatial Index to locate nearby businesses within a radius and uses in-memory caching (e.g., Redis) to serve popular queries with low latency.
This is a specialized index that supports efficient proximity-based lookups. It stores each business's latitude and longitude, maps coordinates to business IDs, and supports fast geo-radius queries.
Business owners use the Listing Service to create or update listings. It handles CRUD operations for business profiles such as name, address, category, hours, and location, and writes updates to the Listings Database. It also emits events via a message queue to trigger search index updates and to invalidate or refresh cached data.
The Review Service manages all user-generated reviews and ratings. It stores reviews in the Reviews Database, calculates and updates average ratings for businesses, and publishes events (e.g., review.created) for downstream consumers.
The User Service manages user accounts, authentication, and user-specific data like favorites and bookmarks. It stores this data in the Users Database.
Media Storage holds photos, logos, and media assets uploaded by business owners or users. Assets are served via CDN for fast delivery, and the media URLs are stored in the Listings or Reviews databases.
The Search Cache is an in-memory cache (e.g., Redis) that serves hot search queries. It improves latency and reduces load on the underlying services and databases.
The message queue powers an event-driven architecture. It decouples write operations from downstream processing, enables asynchronous updates to the search index and cache, and lets services publish and subscribe to domain events such as business.updated and review.created.
The Index Updater is a background consumer that listens to events from the message queue. It updates the geospatial and search index with new or modified business data, ensures eventual consistency between the primary datastore and the search layer, and can scale independently based on indexing workload.
To support millions of users, businesses, and reviews while ensuring low-latency queries and high availability, the system uses a polyglot persistence approach, choosing the right type of database for each microservice based on its data access patterns and scalability needs.
Stores structured metadata about each business.
Recommended Database: PostgreSQL with PostGIS works well when we want strong relational integrity plus geospatial indexing. MongoDB is a good alternative when we prefer flexible, document-based storage.
Schema: businesses
Indexing: We add a geospatial index on location, plus B-tree indexes on category (for filtered search) and average_rating (for sorting or threshold-based queries).
Stores user-generated reviews and ratings.
Recommended DB: We can use either a SQL (e.g., PostgreSQL) or NoSQL database (e.g., MongoDB) since both can meet our requirements.
Schema: reviews
(business_id, timestamp) to retrieve recent reviews quicklyuser_id for showing user’s review historyAggregation: A periodic job or event-driven update to the Listings DB recalculates average_rating.
Stores user profiles, credentials, and favorite listings.
Recommended DB: PostgreSQL or MongoDB
Schema: users
Schema: user_favorites
Enables efficient proximity lookups based on (latitude, longitude).
business_idlat, loncategory, ratingWe will discuss more on this later in the deep dive.
Store business images, logos, and user-uploaded photos associated with reviews.
Recommended Storage: Amazon S3 or any blob store
Metadata Storage: The image URL is stored in the Listings or Reviews DB.
Media is served via CDNs (CloudFront, Cloudflare) to reduce latency and bandwidth usage.
Recommended Storage: Redis Cluster
search:{lat}:{lng}:{category}:{open_now}business:{business_id}reviews:{business_id}Enable asynchronous, decoupled communication between services.
Technology: Kafka or RabbitMQ
business.updated: Triggers search index refresh + cache invalidationreview.created: Triggers rating recalculation + cache updateEach microservice exposes a set of RESTful HTTP APIs. Authentication is handled via OAuth2 or JWT at the API Gateway level.
Let’s walk through the key APIs for each service.
GET /searchDescription: Search for nearby businesses based on location, filters and keyword.
Query Parameters:
lat (required): Latitudelon (required): Longituderadius: Search radius in kilometers (default: 5)category: Business category (e.g., cafe, gym)min_rating: Minimum average rating (e.g., 4.0)open_now: Boolean (true/false)q: Optional keyword (e.g., "vegan", "wifi")Example Request:
Response:
GET /businesses/{id}Description: Get details of a business by ID.
POST /businessesDescription: Create a new business listing (authenticated business owner).
Request Body:
PUT /businesses/{id}Description: Update an existing business listing.
DELETE /businesses/{id}Description: Delete a business listing (rare, admin-only or soft-delete).
GET /businesses/{id}/reviewsDescription: Get all reviews for a business (paginated).
Query Parameters:
page, limitsort_by: recent, rating, helpfulnessPOST /businesses/{id}/reviewsDescription: Submit a review for a business.
Request Body:
PUT /reviews/{review_id}Description: Update your review.
DELETE /reviews/{review_id}Description: Delete your review.
POST /favorites/{business_id}Description: Add a business to user’s favorites.
DELETE /favorites/{business_id}Description: Remove a favorite.
GET /favoritesDescription: Get a list of favorite businesses for the logged-in user.
Efficient proximity search is the backbone of a Yelp-like service. When a user searches for places near their current location, the system must return relevant results in milliseconds even when dealing with tens of millions of businesses.
A naive approach, scanning every record and calculating the distance from (lat, lon), is too slow. Instead, we rely on geospatial indexing to reduce the search space and enable low-latency, scalable lookups.
Let’s explore the most effective strategies and their trade-offs.
Geohashing is a widely used and efficient technique for indexing spatial data. It converts a (latitude, longitude) coordinate into a base-32 encoded string, where geographically close points share common prefixes.
This makes geohash ideal for proximity queries, enabling fast prefix-based lookups in large datasets using standard indexing mechanisms (e.g., B-tree or sorted string index).
In our system, we typically store a 6- or 7-character geohash per business, which gives a good balance between search granularity and index size.
To enable fast geospatial queries using geohash:
geohash column to the listings table.geohash_5, geohash_6).To search for nearby businesses:
geohash LIKE 'prefix%' (e.g., WHERE geohash_6 LIKE 'dpz5%').A geohash prefix query is effectively a range scan on a sorted string, which is significantly faster than computing lat-long distances row-by-row.
Geohashing works well at global scale, but resolution should adapt based on area density:
You can dynamically adjust precision based on the user’s search radius. For example:
For faster lookups, you may store multiple geohash levels (e.g., geohash_5, geohash_6) as separate columns, avoiding the need for LIKE queries.
Geohash indexing is efficient even at large scale:
Sharding Strategy:
Many users search in the same locations (e.g., city centers). These hotspots can be cached for sub-millisecond lookups.
Example Redis Key Structure:
Frequently accessed geohash and filter combinations are stored in Redis or a memory cache. This reduces read load on the database and delivers instant search results for common queries.
A Quadtree is a hierarchical spatial data structure that recursively subdivides a 2D plane into four quadrants: Northwest (NW), Northeast (NE), Southwest (SW), and Southeast (SE). Each quadrant becomes a node, which can be further subdivided if it contains more than a threshold number of data points.
This approach naturally adapts to data density:
This makes quadtrees particularly well-suited for spatial data with uneven distribution, like a city map where downtown has thousands of businesses but the outskirts have very few.
Each node either stores data points directly or pointers to its children. The result is a tree where each node has up to 4 children, and leaf nodes represent spatial buckets.
To perform a proximity search (e.g., "cafes within 2 km"):
This approach is fast and efficient. It avoids scanning irrelevant regions entirely and provides a tighter spatial fit than fixed-grid systems like geohash, especially for irregularly shaped queries.
In practice, quadtrees are typically implemented in-memory on each search server.
At startup, the Search Service loads all business coordinates and builds a quadtree. When a search request arrives, it performs a quadtree traversal to return nearby results.
Despite their theoretical advantages, quadtrees come with several operational complexities:
Building the Tree
Real-Time Updates
Lack of Shared State
Quadtrees can be a great fit when data changes infrequently, fast in-memory search is required, you have tight control over memory and indexing infrastructure, and you’re building a custom geo service optimized for spatial patterns.
In real-world systems, simplicity, reliability, and ease of scaling often take priority over raw algorithmic elegance. That’s why many production systems avoid quadtrees in favor of geohash-based indexing or database-native spatial indexes.
Another common approach to proximity search is to use the native geospatial indexing capabilities built into modern databases.
Instead of managing geohash encodings or building custom data structures, we let the database engine handle spatial indexing internally often using advanced data structures like R-Trees.
Databases like PostgreSQL, MySQL, and MongoDB offer geospatial data types (e.g., POINT, GEOGRAPHY) and spatial indexes optimized for proximity, containment, and range queries.
These systems abstract away the complexity of location indexing, letting you query locations with simple, intuitive functions like:
The database uses a spatial index (typically an R-Tree or variation) to quickly return relevant results.
PostgreSQL + PostGIS
GEOGRAPHY and GEOMETRY types.ST_DWithin, ST_Intersects).MySQL
POINT data types.MongoDB
$near, $geoWithin, $geoIntersects.R-Tree indexes offer higher precision and flexible spatial queries, but come with slightly higher write overhead and storage complexity. Geohash, in contrast, is lighter and simpler but less precise around region boundaries.
Using built-in spatial indexing simplifies early development, but introduces a few constraints:
In our proximity service, database spatial indexes are a solid baseline, especially for early versions of the product.
However, as traffic grows and latency expectations tighten, geohash indexing is preferred for the following reasons:
We may explore quadtrees in the future if we need:
But for now, geohash provides the right balance of simplicity, performance, and scalability.
Let’s say a user opens the app and searches: "Find cafes within 2 km of my current location, open now, and rated 4+ stars."
The mobile/web client makes an HTTP GET request to the API Gateway:
The Search Service constructs a cache key:
It checks Redis for a cached result.
The Search Service:
(lat, lon, radius).The raw result set is filtered based on:
open_hours)If needed, the Search Service fetches additional metadata (e.g., business name, thumbnail) from the Listings DB.
The final set of businesses is:
A lightweight JSON response is constructed with key business fields:
The Search Service stores the response in Redis using the same cache key, with a TTL (e.g., 2-5 minutes) to handle repeated requests efficiently.
The JSON result is sent back via the API Gateway to the client.
Total response time:
Let’s walk through what happens when a business owner creates or updates a business listing (e.g., updates name, address, or hours).
The business owner (authenticated) sends a POST or PUT request to the API Gateway:
business_id (if creating a new listing).geohash for indexing.This decouples the update from downstream processing and improves write latency.
201 Created or 200 OK response to the client.Index Updater
business.updated eventCache Invalidator
This consumer invalidates any cached search results or business detail views involving this listing.
Our system is architected for horizontal scalability at every layer, enabling it to handle millions of users, businesses, and real-time queries efficiently.
Let’s break this down by component:
All core services (Search, Listings, Reviews, Users) are stateless and run behind a load balancer. This allows us to scale linearly by adding more instances.
As traffic grows, we can spin up more containers or VMs (e.g., in a Kubernetes cluster or ECS) to match demand.
The data layer is partitioned (sharded) to prevent any single database instance from becoming a bottleneck:
If we’re storing 200 million business listings, we distribute them across shards using the first few characters of the geohash. Each shard manages a manageable subset (e.g., 1-5 million businesses), allowing for low-latency queries even under global scale.
Reads dominate most user-facing operations (e.g., searching nearby places, viewing business details). To absorb heavy read traffic:
Our architecture is designed to keep most user-facing requests under 200ms, even at scale.
Our primary latency optimization starts with cutting down the search space:
Result: Most geospatial queries complete in under 10ms at the DB level, even for datasets with millions of entries.
We aggressively cache frequently requested data using Redis and local memory caches.
Cache hit path:
Even cache misses are optimized, thanks to indexed reads and minimal post-processing.
To minimize payload size and speed up response times:
This also improves perceived performance. Users see results quickly, and can request more as needed.
When deployed across multiple geographic regions, we route users to their nearest datacenter or edge location:
This drastically reduces network round-trip time (RTT), often the biggest latency contributor in global apps.
For many users, local routing alone can shave off 100+ ms compared to centralized hosting.
To ensure the system remains accessible even in the face of failures, we’ve adopted a multi-layered fault-tolerant architecture that avoids single points of failure, gracefully degrades when needed, and recovers automatically.
All microservices (Search, Listings, Reviews, etc.) are deployed as multiple stateless instances behind load balancers:
Example: If the Search Service has 5 replicas and one goes down, traffic is rerouted without service interruption.
We use replicated, highly available storage systems to avoid data loss and ensure continuity:
Even when parts of the system fail, core functionality remains available. We design for partial availability, not all-or-nothing failure.
In large-scale distributed systems, immediate consistency is expensive and often unnecessary for user-facing features that are not mission-critical.
We deliberately adopt eventual consistency in parts of the system where slight delays in synchronization are tolerable, trading off strict real-time accuracy for higher availability, faster writes, and system scalability.
When a new business is added or an existing one is updated (e.g., name or category), those changes may not immediately reflect in search results.
Users may not see their newly added business in search for a few seconds to a minute, which is acceptable, and the listing is still accessible via direct URL or the dashboard. This decoupling keeps search updates off the write path, which ensures low-latency write operations and higher availability.
After a user submits a new review, the review is saved immediately to the Reviews DB. However, the aggregated average rating and review count shown on business detail pages may lag slightly.
Users get immediate confirmation of their review submission, and the rating or count discrepancy resolves quickly through background aggregators or cache expiration. No global locking or synchronous updates are required, which keeps the system responsive and write-scalable.
This trade-off enables the system to handle high write throughput without bottlenecks.
We allow temporary staleness in data served from caches (e.g., Redis, CDN edge caches).
Users rarely notice a few seconds or minutes of delay in non-critical fields like business name or average rating. This allows for read-scaling and low-latency data access without constant DB hits.
20 quizzes