System design interview

Design a nearby places service

Geohash cells, radius search, ranking, caching and slow business edits.

At a glance

Level
Intermediate
Topics
Location, Search and ranking, Caching
Main parts
API gateway · Search service · Place service · Cell cache · Location index · Places DB · Change queue · Index updater
What you'll learn
Practise this problem
Overview
ClientAPI gatewaySearchservicePlaceserviceCell cacheLocationindexPlaces DBChange queueIndexupdaterClientAPI gatewaySearch servicePlace serviceCell cacheentries per cellLocation indexPlaces DBsource of truthChange queueIndex updater

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. The user opens the map. The client sends their position, a 2 km radius and a category filter.
  2. The gateway applies rate limits and sends the query to a search service instance.
  3. The service turns the point into a geohash cell plus its 8 neighbours and looks each cell up in the cell cache.
  4. For cells that missed, it reads their entries from a location index replica, and keeps them in the cache.
  5. The index returns the entries: place id, position, name, category, rating and opening hours.
  6. The service drops places beyond the radius or closed, ranks the rest and cuts one page of results.
  7. The map shows the first 20 results, with a cursor for the next page.
  • Client
  • API gateway
  • Search service: cells, filter, rank
  • Place service: details and edits
  • Cell cache: entries per cell
  • Location index: replicas, by cell
  • Places DB: source of truth
  • Change queue
  • Index updater
On this page

In 2 minutes

Key ideas
  • The numbers. The whole index is small and almost never written, so many replicas and a cache give cheap reads.
  • Spatial index. Geohash cells with 8 neighbours.
  • Cell size. A cell as wide as the radius, read as a 3 × 3 block.
  • Index freshness. Asynchronous updates through a queue.
  • What to cache. The entries of each cell.
Read the 2-minute answer

I would index places by geohash cell and keep the whole index, about 50 GB, in memory on replicated nodes. A search converts the user's point into a 5-character cell, which is about 4.9 km wide and covers a 2 km radius together with the 8 neighbouring cells. The service reads each cell's entries from a cache, or from an index replica on a miss. It then removes places outside the true radius, applies category and open-now filters, ranks by distance and rating, and returns a page with a cursor.

In a dense city that is about 21,600 candidates for 1,260 real hits, which is cheap to filter in memory. I cache cell entries, not final results, so many users share the cache and filters stay current. Reads scale by adding replicas, six for the 11,600 searches a second peak, and by sharding by prefix if the index grows. Owners write to a places database, the source of truth.

A queue and an updater apply each change to the index within minutes, using a version number so repeats are harmless. The trade-off is a few minutes of delay on edits, in exchange for fast, independent read and write paths.

Step 1 of 6: Clarify the scope

Questions to ask first

Ask these before you draw anything. Guess why each answer changes the design, then open it to check.

What is the largest search radius, and what is the usual one?
Why it matters: The radius sets the cell size and how many cells a search has to read.
How many places are there, and how often do they change?
Why it matters: Mostly static data can be indexed ahead of time and cached. Fast-changing data needs streaming updates.
Do we filter and rank by more than distance, such as category, rating and open now?
Why it matters: Ranking beyond distance means the index must return more candidates than the final page shows.
How soon must a new place or an edited opening time appear in search?
Why it matters: If minutes are acceptable, edits can reach the index asynchronously, which is much simpler than a synchronous write.
Is this location search only, or text search by name as well?
Why it matters: Name search adds a text index. We scope this design to location, category and ranking.

Requirements

Functional

  • Given a latitude, longitude and radius, return the places inside it, nearest first by default.
  • Filter by category and open now, and rank by distance, rating or a blend of both.
  • Page through results without repeats or gaps.
  • Let owners add, edit and remove places, and show the change in search soon after.
  • Show a place's details page.

Non-functional

  • Search: p99 under 200 ms.
  • Full recall: every place inside the radius is returned, sorted by true distance.
  • Owner edits appear in search within 5 minutes.
  • 99.9% availability. Search keeps working while an index is being rebuilt.
  • Handles 11,600 searches a second at the daily peak.

Step 2 of 6: Size it

Show the maths

These numbers are assumptions for the exercise, not facts about a real system:

  • 200 million places worldwide. Each result entry (id, position, name, category, rating, hours) takes about 250 bytes, and a full details record about 1 KB.
  • 100 million searches a day, with a peak of 10 times the average. A typical radius is 2 km.
  • One index replica serves about 2,000 searches a second.
  • Dense city areas hold about 100 places per square kilometre, and owners make 1 million edits a day.

The arithmetic:

Searches per second
100M ÷ 86,400 s ≈ 1,157
≈ 1,160/s, 11,600/s at a 10x peak
Index size
200M × 250 B
50 GB, small enough to hold in memory on a few nodes
Index replicas for peak
11,600/s ÷ 2,000/s per replica = 5.8
6 copies of the index
Candidates in a dense city
3 × 3 cells of ≈ 24 km² = 216 km²; × 100 places per km²
≈ 21,600 places to filter
Places inside a 2 km radius
π × 2² ≈ 12.6 km²; × 100 places per km²
≈ 1,260 places, about 6% of the candidates
Edit rate
1M ÷ 86,400 s ≈ 11.6
≈ 12/s, easily handled asynchronously

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

A search enters through the API gateway and lands on a Search service. It converts the point into a geohash cell, adds the 8 cells around it, and reads each cell's entries from the Cell cache. A miss goes to a Location index replica, which maps a cell to the places inside it.

The service then works on the candidates in memory. It drops places outside the true radius, applies the category and open-now filters, ranks what remains and returns one page. The index entry carries everything a result card shows, so a search never touches the main database.

Owners work through the Place service, which writes the Places DB. That database is the source of truth. Each change is published on the Change queue, and the Index updater applies it to the location index within minutes. Opening a place page reads the full record from the Place service.

Other paths through the system

An edit reaches the index

  1. A business owner changes the opening hours and saves. The owner sees the edit saved from the database at once.
  2. The gateway checks the owner's identity and passes the change to the place service.
  3. The service validates the change and writes it to the places database, the source of truth.
  4. The database change is published on the change queue, with a version number.
  5. The index updater takes the change from the queue.
  6. It rewrites the place's entry, moving it to another cell if its position changed. Search shows the edit within minutes.

Open a place page

  1. The user taps a result to open the place's page.
  2. The gateway forwards the request to the place service.
  3. The service reads the full record: photos, long description, hours and contact details.
  4. The database returns the record.
  5. The service returns the page data.
  6. The client shows the place page.

Step 4 of 6: Go deep on the hard part

Geohash cells or a quadtree

InterviewerHow do you index places so that finding everything within a radius is fast, and how do you choose the cell size?A geohash turns a latitude and longitude into a short string by repeatedly halving the map and recording left or right, up or down.

A geohash turns a latitude and longitude into a short string by repeatedly halving the map and recording left or right, up or down. Each extra character adds 5 bits of precision and shrinks the cell. Places that share a prefix share an area, so the index is just a table from a cell string to the list of places inside it. Near the equator, the cell sizes are roughly:

  • 4 characters: 39 km by 19.5 km.
  • 5 characters: 4.9 km by 4.9 km.
  • 6 characters: 1.2 km by 0.6 km.
  • 7 characters: 150 m by 150 m.

Pick the precision from the radius. Choose the level where the cell's shorter edge is at least the radius. Then the cell holding the user plus its 8 neighbours always covers the circle, because the circle can reach at most one radius past the edge of the user's cell. For 2 km that is 5 characters. For a larger radius, go one level coarser or add a second ring of neighbours: a 5 × 5 block at 5 characters reaches at least 9.8 km from any point in the middle cell.

Why the 8 neighbours. Two points can be a few metres apart and fall in different cells with different prefixes. A search that reads only its own cell would miss places just over the border. Neighbour cells are computed by a small library function.

Wasted candidates. The 3 × 3 block covers about 216 km², and the circle only 12.6 km². In a dense city that is about 21,600 candidates for 1,260 true hits. That is fine for in-memory work, because the exact distance check removes the extras. Smaller cells with more lookups waste less but cost more calls. Both are sound choices, and what matters is saying why.

Dense and empty cells. A city centre cell may hold far more places than a rural one. If a cell grows past a limit, say 5,000 entries, store it as its child cells one level down. A quadtree does this natively: it splits any square with too many places into four. It adapts to density, but it is a pointer structure held in memory, harder to store, shard and update. Geohash strings work in any key-value store, sort in an ordinary index and shard by prefix, which is why I would start there.

Filtering, ranking and paging

InterviewerAfter the cell lookup, how do you filter, rank and page the results?The cell lookup returns candidates, not answers.

The cell lookup returns candidates, not answers. The search service finishes the job in memory, in this order.

  • Exact distance. Compute the real distance of each candidate to the user. For a few kilometres, a flat-earth approximation is accurate enough. Beyond that, use the haversine formula. Drop places outside the radius.
  • Filters. Keep the right category. For open now, store opening hours as a weekly bit pattern in 15-minute slots, which is 7 × 96 = 672 bits or 84 bytes, together with the place's time zone. The service converts now to the place's local time and checks one bit.
  • Rank. Sort by distance for the default view. For a blended view, use a weighted score of closeness, rating and the number of reviews. Use a smoothed rating, such as a Bayesian average, so one five-star review does not beat a thousand good ones.

With about 1,260 places in the radius, a full sort is cheap. To return only the first page, a heap that keeps the best 20 is even cheaper.

Why not cache final results. Radius, category, position and the moment all change the answer, so a cache of final results would almost never hit. Cache the raw entries of each cell instead, which every nearby user shares, and run filtering and ranking per request. Open now is evaluated at request time, so it is always right.

Paging. Offset paging breaks when results move between requests, giving repeats and gaps. Use a cursor: the sort key of the last item shown, such as its score and id. The next request returns the items after that key. For stable ordering across pages, the first request can keep the ranked list of ids for five minutes under a token, and later pages read slices of it.

Scaling reads, and letting slow edits catch up

InterviewerHow do you handle 11,600 searches a second, and how do owner edits reach the index?Reads. The index is small, 50 GB, and it is read far more than it is written.

Reads. The index is small, 50 GB, and it is read far more than it is written. That suits replication more than sharding. Run 6 copies behind a router, and add copies as traffic grows. If the data outgrows one node, shard the index by geohash prefix, say the first 2 or 3 characters, and give busy city shards more replicas. A router sends a query to the shard that owns its cells. A query that touches two shards near a border sends two requests and merges them.

The cell cache. Key it by cell string and store the cell's entries, with a TTL of about five minutes. Hot cells, such as city centres, hit all day. A cold cell is one extra read from the index. The cache also keeps search working for a few minutes if an index replica fails.

Edits are slow on purpose. The places database is the source of truth. An owner's save writes there, and the owner sees it at once because the owner's own page reads from the database. The change is then published on a queue, and the index updater applies it. Reads tolerate minutes of delay, and an edit rate of about 12 a second is trivial. Avoiding a synchronous write to the index keeps owners fast and keeps the index independent of the database's health.

Updates must be safe to repeat. Each change carries a version number from the places database. The updater applies it only if the version is newer than the entry's, so a duplicate or out-of-order message does no harm. If a place moves, the updater removes it from the old cell and adds it to the new one. Then it clears the cache for just those cells.

Repairs. Over time the index can drift from the database, for example after a bug. Rebuild it in the background from the database into a fresh copy and swap it in. This is also how a new field or a new cell precision rolls out safely.

Step 5 of 6: Name the trade-offs

Spatial index
Chose
Geohash cells with 8 neighbours
Over
A quadtree

It is plain strings that any store can sort, shard and cache. The cost is uneven cells, which we handle by splitting crowded cells.

Cell size
Chose
A cell as wide as the radius, read as a 3 × 3 block
Over
Small cells and many lookups

Few lookups per search. The cost is about 17 times more candidates than hits, filtered in memory by exact distance.

Index freshness
Chose
Asynchronous updates through a queue
Over
Write the index and the database together

Edits stay fast and the two systems fail separately. The cost is that a change takes up to a few minutes to show in search.

What to cache
Chose
The entries of each cell
Over
The final results of each query

Many users share a cell, so it hits often, and filters stay current. The cost is that every request still filters and ranks.

Step 6 of 6: Say what breaks next

Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.

How would you add search by place name?
Add a text index of names and categories, and use the geohash cells as a filter on it, so a query for pizza only matches places in the cells around the user. A search engine with a geographic filter does both in one query. The names also feed the autocomplete.
How would you handle a search along a route?
Walk the route and collect the cells along the line, with a buffer of one cell on each side. Read the entries for the union of those cells, then filter by distance to the line instead of to a point. Rank by the detour needed.
What changes if places can move, like food trucks?
Moving places need frequent writes, so the queue-and-rebuild pattern is too slow. Keep those entries in a fast in-memory store with a short expiry and update them on every position report, as a ride-sharing service does for drivers. Static places still use the cached index.
How would you personalise results?
Run the geographic search first to get a few hundred good candidates. Then re-rank only that short list with signals about the user, such as past visits and favourite categories. This keeps the cost of personal ranking small and keeps the geographic part cacheable.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.