InterviewKata
System design · Worked design D14 of 46 · Location and marketplaces · Long form

Design nearby places

"Coffee near me" sounds like a simple filter, but a database index sorts things in one dimension and the map has two. The whole design turns on one idea: turn 2-D positions into 1-D keys (cells) that an ordinary index can range-scan, and choose cells that stay small where places are dense and large where they're sparse. It follows the interview framework phase by phase.

1. The prompt

"Design Yelp, or 'nearby places': users search for businesses near a location, optionally filtered by category and rating, sorted by distance or relevance. Owners add and update listings."

Unlike ride hailing (D13), places barely move: the data changes slowly and is read constantly. The interview is about the spatial index and about serving 50,000 searches a second from something small enough to copy everywhere.

2. Frame the problem

How many places, and how many searches?

About 200M places; 50,000 searches a second at peak.

The index will fit in memory; reads dominate.

What radius?

From about 500 m in a city to 25 km in the countryside.

Cell size must adapt to density and radius.

Filters?

Category, rating, open now; free-text search for names.

A search engine combines text, filters and geo well.

How fresh must edits be?

A new or changed listing can take a few minutes to appear.

The index can be rebuilt or updated in batches.

Do places move?

No (food trucks aside).

Static locations: no high-rate updates, unlike D13.

Ranking?

Distance and rating to start; personalisation later.

Filter by geo first, then rank a candidate set.

3. What it must do

  1. A user can search for places within a radius of a point, with filters.
  2. A user can view a place's details and reviews.
  3. Owners can add or edit places; changes appear within minutes.

Not now:

4. How well it must do it

TargetWhy this number
Fast: results under ~200 ms speedSearch is interactive, often on mobile.
Read-heavy scale: 50,000 searches a second load shapeEdits are ~1 a second; reads are 50,000 times more.
Available: stale by minutes is fine; errors aren't consistency or availabilityA café's listing being a few minutes old harms nobody.
Correct distances: results really are within the radius, nearest first consistency"Near me" that isn't near breaks trust.

5. Numbers that change the design

QuantityWorking→ so we…
Index size200M places × (id 8 B + lat/lng 16 B + cell key 8 B)~6.4 GB → so the geo index fits in memory on one machine; replicate it for reads
Place details200M × ~2 KB~400 GB → so a sharded store, cached for popular places
Search load50,000 a secondA few dozen read replicas or search nodes; caching popular cells helps
Edits~100,000 new or changed places a day~1 a second → so index updates can be batched

6. The nouns

places     place_id, name, lat, lng, category, rating, hours, owner_id, updated_at   (source of truth, sharded by place_id)
geo index  cell_key (geohash / quadtree leaf / S2 cell) → [place_id, lat, lng, category, rating]
reviews    (place_id, review_id) → author, stars, text

7. The contract

GET  /v1/search?lat=…&lng=…&radius=2000&category=coffee&cursor=…   → {places: [...], next_cursor}
GET  /v1/places/{id}       → details
POST /v1/places            → create   ·   PATCH /v1/places/{id} → edit (owner from the session)

Data flow: (1) owners write to the places database → (2) a change stream (or a rebuild every few minutes) updates the geo index replicas → (3) a search finds cells overlapping the circle → (4) exact distance check, filter, rank → (5) hydrate details.

8. First working version

A places table with lat/lng columns and indexes, and a search service that queries it.

Query a bounding box around the point on the indexed columns, then compute exact distances and sort.

8.2 Viewing a place

Read the place and its first page of reviews by id.

8.3 Editing

Owners update their row; the next search sees it.

Parked for later (section 9 fixes each)

  1. An index on latitude scans a band around the whole planet.
  2. Two close places can sit on either side of a cell boundary.
  3. Where the index lives, and how it scales to 50,000 searches a second.
  4. Dense downtown cells versus empty countryside cells.
  5. Ranking, filters and caching popular areas.

9. Make it hold up

9.1 Why a normal index isn't enough, and what is fast

WHERE lat BETWEEN a AND b AND lng BETWEEN c AND d with an index on lat finds every place in a thin band of latitude around the whole planet, then filters by longitude. A composite index on (lat, lng) has the same problem: it is sorted by latitude first. You need a key where nearby on the map means nearby in the index.

ApproachWhy
✗ BadIndex on latitude and longitude columnsOne-dimensional sort order; a radius query scans a huge band.
~ GoodGeohash: interleave the bits of latitude and longitude into one string; a shared prefix means a shared cell (6 characters ≈ 1.2 km × 0.6 km)A plain B-tree can range-scan a prefix. But cells are fixed-size everywhere: a downtown cell holds thousands of places, a rural one none.
✓ GreatAdaptive cells: a quadtree that splits busy cells, or a database's spatial index (PostGIS's R-tree via GiST, a search engine's geo field); or hierarchical cells (Google S2, Uber H3) used at a level matched to the radiusCells stay small where data is dense, so a query looks at few non-matching points. Try it in section 13.

9.2 Edges: searching neighbours too correct distances

Two cafés across the street can be in different cells with different geohash prefixes. A query must cover every cell that overlaps the search circle (typically the centre cell and its 8 neighbours at a suitable precision), then compute real distances to filter and sort. The cells are a coarse filter; the distance check gives the exact answer.

9.3 Where the index lives read-heavy scale

9.4 Dense cities and empty countryside fast

A fixed grid is a bad fit for both: in Manhattan one cell holds thousands of places and every query examines most of them; in the countryside a 2 km search needs dozens of empty cells. A quadtree splits a cell only when it holds more than, say, 100 places, so cell size follows density. With S2 or H3, choose the cell level from the radius: small radius, fine cells; large radius, coarse cells.

9.5 Filtering, ranking and caching fast · read-heavy scale

Filter first by geo and category, then rank the candidate set (distance, rating, open now, personalisation) and paginate with a cursor. Round the query point to a cell and cache results per (cell, radius, category) for popular areas; city centres repeat constantly. Place details come from a cache keyed by place id.

9.6 Keeping the index fresh available

Edits go to the places database, the source of truth. A change stream (change data capture) feeds index updates to every replica within minutes; alternatively, rebuild the whole index every few minutes and swap it atomically. Either way, searches never wait on writes.

9.7 The whole design after the deep dives

A search, and a new café appearing, through the final design.

10. Interviewer follow-ups

Search along a route ("coffee on my way").

Cover the route with a corridor of cells (the cells within a buffer of the path's segments) and rank by detour time from the routing service.

Traffic grows 10×.

More replicas of the in-memory index (it's small), more cache, and CDN caching for anonymous popular searches. The source database barely changes.

"Open now" in every time zone.

Store opening hours with the place's time zone; evaluate at query time in that zone. Precompute an "open now" bitmap every few minutes if the check is too slow.

Food trucks that move.

Treat them like D13 drivers: a small, separately updated in-memory index merged into results.

Personalised ranking.

Geo and filters produce candidates; a ranking model reorders them using the user's history, within a time budget, with distance-and-rating as the fallback.

Why not shard by region?

Popular cities would become hot shards while rural ones sit idle. With a 6 GB index, copying it everywhere is simpler and balances naturally.

Spam listings.

Verification for owners, duplicate detection by name and location, and review-based trust signals before a listing ranks well.

Variation: searching places to stay by date

"Design the search for a holiday-rental site, like Airbnb" adds dates and guests to the map search. A listing matches only if it's in the area, fits the group, and is free every night of the stay, and the price depends on those nights.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Index replica downNothingHealth checksOther replicas serve
Change stream stallsNew edits appear lateIndex age alertSearches keep working on the last index
Places DB slowSlow details pagesRead latencyDetails cache; search results don't need the DB
Hot area (festival)Nothing, ideallyRequest rate per cellCell-result cache; replicas

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (counts, radius, freshness)
4–9Must do, how well, numbers (6 GB index)
9–12Nouns, contract, data flow
12–18First working version, parked list
18–40Deep dives: indexes (9.1), edges (9.2), where it lives (9.3), density (9.4)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–11Must do, how well, numbers
11–15Nouns, contract, data flow
15–23First working version
23–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: routes, open now, moving places; wrap-up
LevelWhat a strong answer shows
Mid-levelWhy lat/lng columns don't index well, geohash or a spatial index, a places DB with read replicas.
SeniorNeighbour cells for edges, geohash vs quadtree vs PostGIS/search engine with trade-offs, the in-memory estimate, caching by cell, filter-then-rank.
Staff+Density-aware cells (S2/H3 levels), replication over sharding for a small hot index, the freshness pipeline from the source of truth, ranking and personalisation, multi-region serving.

Common mistakes:

13. Try it, check yourself, read more

400 places: most in a dense downtown, the rest spread out. Tap the map to move the search circle. Orange cells are the ones the index must read; orange points are examined but outside the circle; teal points are the answers.

Why doesn't an index on (lat, lng) answer radius queries well?

B-trees order by one key; nearby on the map is not nearby in that order.

Why does a geohash query also check neighbouring cells?

The search circle can overlap a cell boundary; neighbours cover it.

What does a quadtree do better than a fixed grid?

Adaptive cells keep the number of points examined per query low in both dense and sparse areas.

The geo index is ~6 GB. Shard it or replicate it?

Replicas spread load evenly; regional shards would create hot cities.
ResourceWhy
Spatial indexing (PostGIS workshop)How a spatial index uses bounding boxes, and how to query it.
S2 cell hierarchy (S2 Geometry)Hierarchical cells on a sphere, mapped to 64-bit keys.
Design Yelp (Hello Interview)Another walkthrough.