Design Yelp / Nearby Places (Proximity Search)
Nearby-places is read-heavy over a near-static POI set, so serve it from a search engine (geo_distance plus attribute filters plus ranking) fed by a denormalized read model, cache popular result pages and detail pages with generous TTLs invalidated on rare edits, and do not over-build the low-rate write path.
Why nearby-places is the opposite of Uber matching
Yelp's "nearby places" looks like Uber matching at first glance (both are "find things near me"), and the whole lesson is why it is actually the opposite workload. In Uber, the points (drivers) move every few seconds, so writes dominate and you keep the index in memory as overwrites. In Yelp, the points (restaurants, shops, POIs) barely move; a place's location changes essentially never, its hours and rating change rarely. The workload is read-heavy over a mostly-static dataset, which flips every design decision toward precomputation, denormalization, and aggressive caching.
Scale assumption: tens of millions of POIs, very high read QPS, queries like "coffee within 2km, open now, sorted by rating and distance." The spatial part is only half the query; the other half is attribute filtering (category, open-now, price, minimum rating) and ranking.
The spatial index is a search engine
You still bucket coordinates into cells (geohash, quadtree, or S2), so a radius query hits a cell plus its neighbors. But instead of a bespoke in-memory geo service, the natural home is a search engine (Elasticsearch/OpenSearch) with a native geo_distance filter, because it does spatial filtering, attribute filtering, full-text ("coffee"), and ranking in one query. This is the key architectural difference from Uber: Yelp's index is a search index you can rebuild from source, not a volatile live index.
source of truth (Postgres/doc store) --pipeline--> denormalized read model in Elasticsearch (geo + attrs + text)
place edits/new reviews (low rate) --> update pipeline --> reindex
query --> [ES: geo_distance cell + filters + rank] --> results, with popular (cell,filter) pages cached
Query flow, storage, caching
Query flow: (1) candidate generation by spatial cell/radius; (2) attribute filter: category, open-now (computed from stored hours plus current time), price band, minimum rating; (3) rank by a blend of distance, rating, review count/popularity, and sponsored boost.
Source of truth for places, reviews, and edits lives in a relational or document store. A denormalized read model (the ES index) is what queries hit. Place detail pages go in a KV cache (Redis). Photos and media sit on a CDN.
Because the underlying data is stable, you cache hard: popular (cell, filter) result pages and place-detail pages get generous TTLs (minutes to hours). A search for "coffee near downtown SF, open now" is asked constantly and its answer barely changes, so it should be served from cache the overwhelming majority of the time. Invalidate on the rare place update rather than expiring everything constantly. Reads scale with replicas (ES read replicas) and CDN edge caching.
New reviews, edits, and new places are comparatively low-rate. They update the source of truth, then flow through an indexing pipeline that updates the ES read model and invalidates affected cache entries. You never optimize this path for high throughput because the workload does not have it.
Interview nuance: the trap is to over-engineer the write path. If you find yourself building a high-frequency location-write ingestion system or geofencing with constant updates, you have modeled Yelp like Uber and wasted your design budget on throughput the workload never generates. The senior move is to explicitly state "this is read-heavy and mostly static, so I precompute and cache instead of optimizing writes."
Recap: nearby-places is read-heavy over a near-static POI set, so serve it from a search engine (geo_distance plus attribute filters plus ranking) fed by a denormalized read model, cache popular result pages and detail pages with generous TTLs invalidated on rare edits, and do not over-build the low-rate write path.
Apply
Your turn
The task this lesson builds to.
Design Yelp's 'nearby places' feature: given a user location and filters, return ranked places within a radius, and justify your spatial index, ranking, and caching for a read-heavy, mostly-static dataset.
Think about
- How is this different from Uber matching, where points move every few seconds?
- How do you combine spatial filtering with attribute filters (open now, category, rating) and ranking?
- What is cacheable when the underlying place data barely changes?
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Design the 'restaurants near you, open now, delivering to your address' search for a food-delivery app (Uber Eats scale) where, unlike Yelp, availability is genuinely dynamic: a restaurant can go offline, hit capacity, or stop delivering to your zone within minutes. Explain how you keep Yelp-style read caching while a slice of the data is now fast-changing.
Think about
- How do you split the data by velocity (static attributes vs live availability)?
- How does a real-time overlay stage keep the search itself cacheable?
- Why is putting availability into the ES index the wrong turn?