Design problems
Search & discoveryMediumSDE-2 30 min

Design nearby places (Yelp / Google Maps places)

Asked at
GoogleMakeMyTripOlaSwiggyUberZomato

The interviewer asks

“Design a service like Yelp or the places layer of Google Maps: given a user's location, return nearby businesses (restaurants, ATMs, petrol pumps, hospitals), optionally filtered by category or a text query, and show each place's details and reviews.”

What a strong answer looks like

A strong answer notices that this is a read-heavy system with slowly changing data: ~200M places, very few writes, and very many location queries. Places live in a relational or document database as the source of truth; a geospatial index (geohash or S2/H3 cells, or a quadtree) maps cells to place IDs so a 'nearby' query becomes a lookup of the user's cell plus neighbours, followed by exact distance filtering and ranking. Because the data barely changes, the geo index can be rebuilt or updated asynchronously and replicated widely, with heavy caching of place details and popular cell results. Text search ('dosa near me') uses Elasticsearch with geo filters. The candidate should compare geohash, quadtree and S2/H3, handle dense vs sparse areas with adaptive radius, and keep reviews and photos in separate services and stores.

Concepts to mention

Geohash / S2 / H3 cell indexingQuadtree for density-adaptive cellsNeighbouring cells plus exact distance filterRead-heavy: replicate the in-memory indexAdaptive radius for sparse areasRanking blend: distance decay + Bayesian ratingAsync index updates from a change streamReviews partitioned by place_id

1 Requirements

Functional

  • Search places near a location within a radius (500 m to 20 km), with an optional category filter
  • Text search near a location ("south indian breakfast", "24 hour pharmacy")
  • Show place details: name, address, hours, phone, rating, photos
  • Read and write reviews and ratings
  • Business owners can add or update a place (moderated)
  • Sort by distance, rating or relevance; filter 'open now'

Non-functional

  • Nearby search p99 under 200 ms
  • Read-heavy: ~100k search QPS at peak vs ~100 place updates/s
  • Place edits visible in search within ~1 hour (strong freshness not needed)
  • Availability 99.99% for search; reviews can be eventually consistent
  • Global coverage with India-specific needs: dense cities, poor addresses, landmarks

Out of scope (say it out loud)

  • Turn-by-turn navigation and routing
  • Real-time moving objects (drivers, friends) - see friends-nearby and ride-hailing
  • Map tile rendering
  • Ads and sponsored listings

2 Estimates

Places~200M worldwide (~20M in India)Includes shops, restaurants, ATMs, offices; long tail of small businesses
Place data size~200 GB core + ~50 TB photos200M x ~1 KB metadata ~ 200 GB; photos ~10 per popular place x 2M places x 2 MB in object storage
Geo index size~4-6 GB200M entries x (8-byte cell ID + 8-byte place ID + lat/lng 8 bytes) ~ 4.8 GB: fits in RAM on one machine, so replicate rather than shard
Search QPS~30k/s avg, ~100k/s peak~500M searches/day / 86,400 ~ 5.8k/s for one product; a maps-scale product with many surfaces sees several times more; lunch and dinner peaks ~3x
Writes~50-100/s (places), ~500/s (reviews)Edits and new places are rare; reviews are more frequent but still tiny vs reads
Read servers~20-40 geo index replicasOne in-memory replica handles ~5k queries/s (cell lookup + distance sort of a few hundred candidates); 100k / 5k = 20, x2 for headroom and regions

3 API

EndpointWhat it does
GET/v1/places/nearby?lat=&lng=&radius_m=2000&category=atm&open_now=true&sort=distance&page_token=Places near a point, filtered and ranked
GET/v1/places/search?q={text}&lat=&lng=Text search biased to the user's location
GET/v1/places/{place_id}Place details with rating summary and top photos
GET/v1/places/{place_id}/reviews?cursor=Paginated reviews
POST/v1/places/{place_id}/reviewsAdd a review (rating 1-5, text, photos); idempotency key from client
POST/v1/placesAdd or suggest an edit to a place; goes to moderation

4 Data model

placesPostgres (with PostGIS) or MySQL, sharded by place_id at large scale
place_id (PK)namecategory_ids[]latlngaddresscityphonehours JSONrating_avgrating_countstatusupdated_at

Source of truth. Small enough (~200 GB) for a few primaries plus read replicas; heavy reads served by caches.

geo_indexIn-memory service (built from places) or Redis GEO
cell_id (S2 level ~13 / geohash 6) -> [place_idlatlngcategory_bitmaskrating]

Read-only replica per node, rebuilt or patched from a change stream. Redis GEOSEARCH uses geohash-encoded sorted sets and works well for a single category set.

places_searchElasticsearch / OpenSearch
place_idnamename_translitcategoriestagslocation (geo_point)ratingpopularity

For text queries with geo_distance filters and decay functions.

reviewsCassandra / DynamoDB
place_id (partition key)created_at + review_id (clustering)user_idratingtextphoto_ids

Write-friendly, read by place in time order. Rating aggregates are updated asynchronously into places.

photosS3 + CDN
photo_idplace_iduser_ids3_keysizes

Resized variants served from CDN.

place_cacheRedis
place:{id} -> details JSON (TTL 1 h)

Hot places (malls, stations) read constantly.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALimagesnearby / searchdetailscells -> place IDstext + geobatch detailsplace changedrating changedMobile / web clientAPI gateway / LBCDN (photos)Nearby search servicePlace serviceReview serviceGeo index / ES indexerKafka (place changes)Geo index (in-memory)ElasticsearchPlaces DB + RediscacheReviews (Cassandra)Photos (S3)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Find ATMs within 1 km

  1. 1Client sends GET /v1/places/nearby?lat=12.9716&lng=77.5946&radius_m=1000&category=atm.
  2. 2Nearby search service computes the covering cells for a 1 km circle: with geohash precision 6 (~1.2 km x 0.6 km cells) that is the user's cell plus its 8 neighbours; with S2 it asks for a covering of the circle at a suitable level.
  3. 3It reads the place lists for those cells from its in-memory geo index and keeps only entries whose category bitmask includes ATM.
  4. 4For each candidate it computes the exact haversine distance and drops anything beyond 1 km (cells are squares, the query is a circle).
  5. 5If fewer than ~10 results remain (sparse area), it widens the radius step by step up to a limit.
  6. 6It ranks by distance (or a blend of distance, rating and popularity), applies 'open now' using hours in the user's time zone, and batch-fetches details for the top 20 from the place cache.
  7. 7Response returns 20 places and a page token; photo URLs point to the CDN.

Owner updates opening hours

  1. 1Owner submits an edit; the place service validates it and puts it in a moderation queue (automatic checks plus human review for risky fields like name or location).
  2. 2Once approved, the place row is updated in the database and the Redis detail cache entry is deleted.
  3. 3A change event goes to Kafka; the indexer updates Elasticsearch and patches the geo index on each replica (move the place between cells if lat/lng changed).
  4. 4Search reflects the change within minutes; the nightly full rebuild of the geo index corrects any drift.

7 Deep dives

Choosing the geospatial index

A plain B-tree on (lat, lng) does not work well: a range on latitude plus a range on longitude scans a wide strip. You need a structure that maps 2D closeness to 1D keys. Geohash interleaves latitude and longitude bits into a base-32 string; a shared prefix means nearby, so you can store places by geohash prefix and query the user's cell plus 8 neighbours (neighbours are needed because two points across a cell edge can share no prefix). Precision 6 gives ~1.2 x 0.6 km cells, precision 5 ~4.9 x 4.9 km. A quadtree splits a region into four until each leaf holds at most ~100 places, so dense Connaught Place gets tiny cells and rural Rajasthan big ones; it adapts to density but must be built in memory and is harder to update and shard. Google S2 (and Uber's H3 with hexagons) are hierarchical cell systems on the sphere with good coverings for circles and polygons and 64-bit cell IDs that sort well. Good answer: S2 or geohash cells as keys in an in-memory index, with an explanation of why neighbours and exact distance filtering are needed.

Why replicate, not shard, the geo index

The full geo index is about 5 GB: 200M places x ~24 bytes. That fits in memory on any modern server. So instead of sharding by region (which makes queries near region borders touch two shards and creates hot shards for Mumbai and Delhi), give every search node a full copy and scale reads by adding nodes. Updates are rare (~100/s), so each node consumes the change stream from Kafka and patches its copy, and a nightly snapshot from the database rebuilds it from scratch to remove drift. A new node starts by loading the latest snapshot from S3 and then replaying Kafka from the snapshot's offset. If the place count grew 10x and the index no longer fits, shard by S2 cell ranges at a coarse level and route queries to the one or two shards their covering touches. Saying 'it fits in memory, so replicate it' with the numbers is a strong signal of estimation-driven design.

Ranking and the dense vs sparse problem

Pure distance sorting is rarely what users want: the closest restaurant may have a 2.1 rating. A useful score blends distance decay, rating (with Bayesian smoothing so a 5.0 from 2 reviews does not beat a 4.5 from 3,000), popularity, open now and text relevance. Elasticsearch function_score with a gauss decay on distance plus field value factors expresses this well for text queries. Density varies hugely: within 500 m of Bandra station there are thousands of places, while a highway stretch may have none within 10 km. Use an adaptive radius: start with a small covering, and if too few candidates match the filters, expand to coarser cells. Cap the candidate count per query (e.g. 1,000) by taking the best-scored places per cell, which you can pre-sort by popularity inside each cell list. For India, addresses are weak, so landmarks ('near Hanuman temple') in search fields and user-reported pin corrections matter.

Reviews and rating aggregates

Reviews are write-heavy compared with places and have a natural partition key, place_id, with time-ordered clustering, so Cassandra or DynamoDB fits. Do not recompute the average on every read. When a review is created, publish an event; a consumer increments rating_sum and rating_count for the place (idempotently, keyed by review_id so retries do not double count) and updates the place row and search index. Spam and fake reviews are a real problem: limit reviews per user per place, weight by reviewer trust, detect bursts from new accounts, and hold suspicious reviews for moderation before they count. Show a Bayesian average so a new place is not ranked at the top on one 5-star review.

8 Trade-offs

Geohash vs quadtree vs S2/H3

Geohash is simple and works in any key-value store or Redis GEO, but cells are distorted and need neighbour lookups. A quadtree adapts to density but is in-memory and awkward to update. S2/H3 give better coverings and hierarchy at the cost of a library dependency. Pick S2 or geohash and justify it.

PostGIS alone vs a dedicated geo service

PostGIS with a GiST index handles nearby queries well up to a few thousand QPS and is a great v1. At ~100k QPS, a replicated in-memory index is cheaper and faster, with Postgres kept as the source of truth.

Freshness vs simplicity

Patching the index from Kafka gives minutes of freshness; a nightly rebuild alone is simpler but a new restaurant would wait a day. Use both: streaming patches plus nightly rebuild for correctness.

One system for text + geo or two

Elasticsearch can do both and is enough for text queries. For category-only 'nearby' queries the in-memory index is faster and cheaper, so use it for the hot path.

9 Bottlenecks

Hot areas

Queries around airports, stations and malls hit the same cells. Cache results per (cell, category, sort) for 5-10 minutes; place data barely changes.

Place details fan-out

Each search needs details for 20 places. Use batch reads (MGET) from Redis and keep the summary fields needed for a list card inside the geo index entry, so the list renders without the details call.

10x growth in places

Index grows to ~50 GB: still fits in RAM on large machines, or shard by coarse S2 cell ranges with routing by covering.

10 Failure handling

Places DB primary fails

Reads are unaffected (geo index and caches serve them). Writes fail over to a replica promoted by the database's HA setup; edits queued by clients retry with idempotency keys.

Geo index node loses its state

It reloads the snapshot from S3 and replays Kafka; the load balancer keeps it out of rotation until it reports ready.

Elasticsearch cluster degraded

Text search falls back to the geo index with name prefix matching on the candidate set; results are less relevant but still useful.

11 What impresses

  • Computing that the geo index fits in RAM and choosing replication over sharding
  • Explaining why you must query neighbour cells
  • Bayesian average for ratings
  • Adaptive radius for dense vs sparse areas
  • Snapshot plus change-stream replay to bring up new index nodes
  • Keeping list-card fields inside the index to avoid a fan-out

12 Mistakes

  • SELECT ... WHERE lat BETWEEN ... AND lng BETWEEN ... on a big table without a spatial index
  • Querying only the user's own geohash cell
  • Sharding by city and ignoring border queries
  • Treating places like real-time moving objects and over-engineering writes
  • Recomputing average ratings on every read
  • Ignoring sparse areas where a fixed radius returns nothing

13 Follow-ups

Q1How would you support 'along my route' searches?
Q2How would you handle the user moving while scrolling results?
Q3How do you detect duplicate places added by different users?
Q4How do you compute 'open now' correctly across time zones and holidays?
Q5How would you add real-time 'busy now' information?
Q6How would you rank sponsored places without misleading users?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free