DEV Community

Cover image for Design Yelp: the proximity-search system design walkthrough
Gowtham Potureddi
Gowtham Potureddi

Posted on

Design Yelp: the proximity-search system design walkthrough

Design Yelp: the proximity-search system design walkthrough

The request to design Yelp sounds like a database with a map on top — a table of businesses, a search box, a list of results — and that picture is exactly what the round is built to puncture. The moment you say "show me the restaurants near me," you have asked a question a normal table cannot answer: find every business within one kilometre of this point, out of fifty million rows, in a few milliseconds. This is one of the most revealing proximity search system design prompts because it is not really about storing businesses or serving reviews — it is about geospatial search: turning "near me" into something an index can look up without scanning the world.

The round turns on one structural insight. "Find places near me" is a geospatial range query, not CRUD over a businesses table — and the naive answers both fail in a way the interviewer is waiting for. You cannot scan all fifty million rows and compute a distance for each on every search, and a plain B-tree index on latitude and longitude does not save you either: it narrows one dimension but still leaves a long thin strip of the map to scan. The job is to map two-dimensional proximity onto a one-dimensional, indexable key so that "nearby" becomes "shares a prefix" or "lives in the same cell." That has three well-known answers — a geohash (encode the coordinates into a prefix string), a quadtree (recursively subdivide the map until each cell is small), and Google S2 (number cells along a space-filling curve) — and naming them, picking one, and defending the choice under uneven density is the spine of the round. Get that reframe — a range query answered by a cell index, then refined with exact distance — and the rest of the system design interview is deciding how a radius becomes a set of cells, how you avoid missing a result sitting just over a boundary, and how the whole thing stays fast when it is read a hundred times for every write. This walkthrough follows the path a strong candidate draws on the whiteboard — requirements, estimate, API and data model, high-level design, then the deep dives — naming the follow-up the interviewer uses at each step to tell a memorised answer from real understanding.

Title card for a system design interview walkthrough on designing Yelp-style proximity search, showing a map pin at the centre of a search radius circle laid over a grid of cells, with only the handful of cells the circle touches lit up and the businesses inside them returned as nearby results, on a dark citrus-and-lavender background.

How the proximity-search round is actually run

A proximity-search round runs about 45 minutes on a shared canvas — you draw the boxes and arrows while you talk, and the interviewer interrupts. It is graded as a Medium system design problem, and that matters for how you spend your time: the win here is not surviving some exotic scale, it is reaching a clean geospatial index quickly and defending the one trade-off that actually bites — what happens when density is wildly uneven. A candidate who nails the index, the query flow and the density story, and leaves the review-and-photos features for later, is doing exactly what the round rewards.

The interviewer rarely lets "search the businesses table" stand. "A user is standing downtown and wants every restaurant within one kilometre, ranked by distance — there are fifty million businesses in your system. Walk me through what your service actually does, and don't tell me it scans the table." That question — turning a radius into a bounded lookup — is the one a copied answer can never rehearse, and the one this round is really testing.

The rounds here are modelled on how the loop runs, not scraped from any candidate's recall. What matters for prep is the shape: a blank canvas, a Medium-weight problem with one sharp trade-off, and a follow-up after every decision you make.

The five-step framework for any HLD round

Every high-level design answer fits the same spine. Say it out loud at the start so the interviewer knows you have a plan, then walk it:

  1. Clarify requirements — functional and non-functional, and what you are explicitly not building.
  2. Estimate — number of businesses and search queries per second versus write rate, to justify every later choice.
  3. High-level design — the boxes and arrows: the clients, the search service, the geospatial index, and the business-metadata store.
  4. Deep dive — the two or three decisions that actually decide this system.
  5. Follow-ups — bottlenecks, failure modes, and what you would change at ten times the scale.

What the debrief measures

The score is not "right or wrong design." A good HLD debrief reads on four dimensions, and every move below maps to one: problem solving (did you reframe it as a geospatial range query and reach a cell index, naming geohash, quadtree and S2), communication (did you say in one sentence how each index maps 2D proximity to a 1D key and when you'd pick each), technical execution (covering cells plus their neighbours, an exact haversine filter, ranking and pagination, with the geo index kept separate from the metadata), and time management (did you spend your 45 minutes on the index, the query flow and density, not on the review system). Reading a walkthrough teaches the design; it does not teach you to deliver it on the clock while someone pushes back.

Step 1 — Requirements: what a proximity search service must do

Spend the first few minutes here. Jumping to boxes before you have scoped the problem is the fastest way to design the wrong system.

Type Requirement
Functional A user searches for businesses within a radius of a location (or a map viewport) and gets them back ranked; each result shows name, category, rating and distance; a user can open a business and read its details; businesses can be added and their details updated.
Functional (optional) Filter by category or "open now", sort by rating versus distance, full review read/write, photos, autocomplete on business names — call these out, then scope most of them for later.
Non-functional Low-latency reads — a search returns in milliseconds; the system is heavily read-dominated and business data changes slowly, so eventual consistency on a business edit is fine; high availability; scale to tens of millions of businesses and a high search query rate.

The sentence that earns points: "This is a geospatial range query, not CRUD — I can't scan fifty million rows per search and a plain latitude/longitude index doesn't help, so the core of the design is a spatial index that maps 2D proximity to a 1D key I can look up." Say that and you have framed the entire design before drawing a box.

Step 2 — Capacity estimation (back of the envelope)

Estimation here is about proving one thing: the load is overwhelmingly reads against mostly static data, and the only hard part is the spatial lookup — everything else is a cacheable, replicable read store.

Quantity Assumption Result
Total businesses tens of millions worldwide a large but bounded dataset — small enough to index in memory per region, too large to scan per query
Search queries very high rate, every map pan and "near me" tap read-heavy; the spatial lookup must be an indexed cell read, never a scan
Writes (new / edited business) rare, and tolerant of a short delay the index and metadata can be updated lazily; no write-path pressure
Density per area wildly uneven — a downtown cell may hold thousands, a rural cell a handful the index must cope with non-uniform density, the trade-off that decides geohash vs quadtree

Two conclusions fall out, and you should say both out loud. First, the system is read-heavy and slow-changing, so after the index is right, scaling is the easy part: cache popular searches, serve metadata from read replicas, and never let a write path become the bottleneck. Second, there are two layers that should be kept apart — the spatial layer that answers which businesses are near this point (the geo index, the hard problem) and the metadata layer that answers what are they (name, hours, rating, reviews — a normal, cacheable store). You have now pointed the interview at the real deep dives — the index, the query flow, and density — before anyone asked.

Step 3 — API and data model

The API is deliberately small: a client searches around a point, opens a business, and occasionally a business is written.

Operation Shape
search_nearby(lat, lng, radius, filters, page) the core read — returns businesses within the radius, ranked, paginated.
get_business(business_id) one-off read of a single business and its details.
add_or_update_business(business) writes name, location, category and attributes; updates both the index and the metadata store.
search_by_viewport(bounding_box, filters) map-pan variant — returns businesses inside a rectangle instead of a radius.

The data model splits along the two layers. The geo index is keyed by spatial cell; the business metadata is a plain record read far more often than it is written:

Entity Notes
business business_id, name, lat, lng, category, rating, cell_key — the metadata record, stored in a normal, replicated store.
geo_index cell_key → [business_id, …] — the spatial index that turns a cell into the businesses inside it (geohash prefix, quadtree node, or S2 cell id).
review (business_id, user, stars, text) — part of the metadata layer; scope the full review system for later in this round.

Step 4 — High-level design: the search service, geo index and metadata store

Now the boxes. A client sends a location and radius; a search service turns that into a cell lookup, fetches candidates, refines and ranks them, and reads the details from a separate store:

  • API gateway: terminates the client request and routes search_nearby to the search service.
  • Search service: the heart of the system. It computes the covering cells for the radius, reads the candidate business ids from the geo index, computes exact distance to drop false positives, ranks the survivors, and paginates.
  • Geospatial index: maps a cell key to the business ids inside it — a geohash prefix, a quadtree node, or an S2 cell id. This is the structure that makes "near me" an indexed read instead of a scan.
  • Business metadata store: the businesses themselves (name, hours, category, rating, reviews), fronted by read replicas because it is read far more than written.
  • Cache: popular searches and hot cell → businesses results sit in a cache, since the same busy areas are searched over and over.
  • Write path: adding or editing a business is rare and slow-changing — it updates the metadata store and the geo index, and the caches expire naturally.

Say explicitly that the design has two layers kept apart: the spatial layer answers which businesses are near, and the metadata layer answers what they are. Naming that split — a search service over a cell index, backed by a separately scaled read store — is a signal the interviewer is listening for.

A proximity search architecture diagram showing clients sending a search-nearby request through an API gateway to a search service, which queries a geospatial index that maps map cells to business ids, fetches the matching businesses from a separate business-metadata store served by read replicas, and reads through a cache of popular searches and cell-to-businesses results, with a slow write path that updates both the geo index and the metadata store.

Deep dive: the geospatial index (geohash vs quadtree vs S2)

This is the decision the whole round is really about. The follow-up is the one from the top: find every restaurant within one kilometre of me — how, without scanning all fifty million businesses?

Start by killing the naive answers out loud. A full scan computes a distance for every business on every search — impossible at this scale. A plain B-tree on latitude and longitude looks tempting but barely helps: an index on lat narrows you to a horizontal band of the whole planet, an index on lng to a vertical one, and you are still left scanning a huge strip — two 1D indexes do not compose into a 2D range. The fix is to map 2D proximity to a single indexable key, so that points near each other on the map are near each other in the index. There are three standard ways, and a strong answer names all three before picking one:

Geohash Quadtree Google S2
Idea interleave the bits of lat and lng and base-32 encode them into a prefix string; nearby points share a prefix recursively split each square into four quadrants until a cell holds at most N points number cells along a Hilbert curve so each cell is a 64-bit id and nearby cells have nearby ids
Density fixed grid — every cell is the same size regardless of how many points fall in it adapts — dense areas subdivide deeply, sparse areas stay coarse fine-grained levels; handles the whole globe and the poles cleanly
Where it lives any string-prefix index — a plain DB column or Redis an in-memory tree that must be built and rebalanced a purpose-built library/service
Best fit simple, portable, "good enough" nearby search uneven density, where a fixed grid breaks down planet-scale, Google/Tinder-class systems

For a Yelp-style service, start with a geohash: it needs no special infrastructure — a geohash prefix is just a string column with a B-tree index, and a radius query becomes "the handful of geohash cells that cover the circle." Then say the one sentence that shows range: all three turn 2D proximity into a 1D indexable key — geohash with a fixed grid of prefixes, a quadtree with an adaptive tree, S2 with a Hilbert-curve id — so I'd stay on geohash for simplicity and reach for a quadtree once density is uneven enough that a fixed cell size over-returns downtown and under-fills the countryside. That is the trade-off the round is testing, and it sets up the density deep dive.

"Find every restaurant within one kilometre of me — how, without scanning all fifty million businesses?" I don't scan, and I don't rely on a plain lat/long index — two 1D indexes still leave a strip to scan. I give every business a spatial cell key: a geohash prefix, where nearby points share the prefix. A radius query becomes the small set of geohash cells that cover the circle, so I read only the businesses in those cells — a few hundred candidates, not fifty million — and then refine with exact distance. If density were very uneven I'd use a quadtree so cells adapt to how many businesses are in each area.

A diagram comparing three ways to index points on a map so a nearby-search becomes a cell lookup: geohash encodes latitude and longitude into a base-32 prefix string so nearby points share a prefix on a fixed uniform grid, a quadtree recursively splits each square into four until a cell holds at most N points so dense areas subdivide deeply and sparse areas stay coarse, and Google S2 numbers cells along a Hilbert curve so each map cell becomes a single 64-bit id with nearby cells having nearby ids, all three turning two-dimensional proximity into a one-dimensional indexable key.

System Design
HLD · Search & discovery · 45 min
Design Yelp proximity search out loud, with follow-ups

Practise on Zynter →

Deep dive: the search query flow

The index decides how points are stored; this decides what actually happens on a single search and why the cells are only half the answer. The follow-up is about the edges: a result sits just over a cell boundary from me — how do you not miss it, and how do you keep the results exact?

The flow is a coarse filter then a fine filter: cells get you a small candidate set cheaply, and exact distance makes it correct.

  • Compute the covering cells: from the user's location and radius, work out the set of cells (geohash prefixes, quadtree nodes or S2 cells) that overlap the search circle. Pick a cell precision whose size is close to the radius so the set stays small.
  • Include the neighbours: a business just over a cell boundary is still within the radius, so always query the covering cells plus their adjacent cells. This is the classic boundary bug — skip the neighbours and you silently drop nearby results.
  • Fetch the candidates: read the business ids in those cells from the geo index. This is a bounded set — a few hundred or thousand — not the whole table.
  • Refine with exact distance: the cells are rectangles, the search is a circle, so compute the exact haversine distance from the user to each candidate and drop any that fall outside the true radius. The index is approximate on purpose; haversine makes the answer precise.
  • Rank and paginate: sort the survivors by a blend of distance, rating and relevance, then return a page at a time so the client never pulls thousands of rows.

The point to state plainly: the geo index is a coarse filter that trades precision for speed, and the exact-distance step is what makes the result correct — and you must always query the neighbouring cells, because the one place a naive version breaks is a result sitting a few metres over a boundary. Instant "near me" is a cheap cell read followed by a precise distance check, never a scan.

"A result sits just over a cell boundary from me — how do you not miss it, and how do you keep the results exact?" I never query just the single cell the user is in. I compute all the cells that cover the radius and include their immediate neighbours, so a business a few metres over a boundary is still a candidate. Then I compute the exact haversine distance for every candidate and drop the ones outside the true radius — the cells are a coarse filter, the distance check is the precise one. That way nothing near the edge is lost and nothing outside the circle sneaks in.

System Design
HLD · Search & discovery · 45 min
Defend the covering-cells + haversine flow out loud

Practise on Zynter →

Deep dive: density, scale and read-heaviness

Getting the index and the query flow right keeps a single search correct; this decides whether the design holds up across a real map and a real load. The follow-up is the one that separates a memorised answer from an understood one: one downtown cell has ten thousand places and a rural cell has two — how does your index handle both, and how do you serve a hundred times more reads than writes?

Two forces shape this answer — uneven density and a read-heavy, slow-changing workload:

  • Density is the real trade-off: a fixed-grid geohash uses one cell size everywhere, so the dense downtown cell returns ten thousand candidates you must then distance-filter, while a rural cell of the same size holds almost nothing. A quadtree adapts: it subdivides the dense region into many small cells and leaves the empty region as one large cell, so every cell holds a roughly bounded number of businesses. Saying that out loud — fixed grid is simpler but struggles with uneven density; the quadtree subdivides only where it is dense — is the single most important sentence in this round.
  • Tune geohash precision as a cheaper fix: if you want to stay on geohash, use a longer prefix (smaller cell) in dense areas and a shorter one in sparse areas, which approximates what the quadtree does for free.
  • The load is overwhelmingly reads: people search far more than businesses change, so cache popular searches and hot cell → businesses results; the same busy areas are queried constantly and the data behind them barely moves.
  • Scale reads with replicas: serve the business-metadata reads from read replicas, and keep them separate from the spatial lookup so a hot metadata read does not contend with the index.
  • Writes are easy: a new or edited business is rare and tolerant of a short delay, so the write path updates the index and metadata lazily and lets caches expire — there is no write bottleneck to engineer around.

The point to state plainly: the hard part of scaling this system is not throughput, it is density — the index must not blow up in a dense cell or waste space in an empty one — and once the index handles density, the read-heavy load is absorbed almost entirely by caching and read replicas over slow-changing data. Keep the geo index and the metadata store separate so each scales on its own terms.

A diagram contrasting how a fixed-grid geohash and an adaptive quadtree handle uneven business density, showing a dense downtown cell holding ten thousand businesses and a rural cell holding two, where the fixed grid either over-returns from the dense cell or under-fills the sparse one while the quadtree subdivides the dense region into smaller cells and leaves the sparse region coarse, alongside the read-scaling picture of a cache for popular searches and read replicas serving a workload with far more reads than writes.

The follow-ups that decide the proximity-search round

Scan the probes from each deep dive and they rhyme. Master these five and you are ready for a version of the question that is not drawn exactly like this one.

  • "Why not just index latitude and longitude and query a range?" → Because two separate 1D indexes don't compose into a 2D range — each narrows one axis to a planet-long strip you'd still scan. You need a single spatial key (geohash, quadtree or S2) so points near on the map are near in the index. (problem solving)
  • "Geohash, quadtree or S2 — which and why?" → Geohash for a simple, portable index that's just a string column; a quadtree when density is uneven, because it subdivides dense areas and leaves sparse ones coarse; S2 for planet-scale with a Hilbert-curve cell id. All three map 2D proximity to a 1D indexable key. (communication)
  • "A result sits just over a cell boundary — how do you not miss it?" → Query the covering cells plus their neighbours, then compute exact haversine distance and drop anything outside the true radius; the cells are a coarse filter and the distance check is the precise one. (technical execution)
  • "One cell has ten thousand businesses and another has two — how does your index cope?" → A fixed-grid geohash over-returns in the dense cell and wastes the sparse one; a quadtree adapts by subdividing only where it is dense, or you tune geohash precision per area to approximate it. (technical execution)
  • "This is a hundred times more reads than writes — how do you scale?" → Cache popular searches and hot cell-to-businesses results, serve metadata from read replicas, and keep the geo index separate from the metadata so each scales independently; writes are rare and can be lazy. (problem solving)

Each maps to a debrief dimension. Reading this list teaches you the answers; it does not teach you to deliver them while someone interrupts — that only comes from saying them out loud.

How to prepare: a one-week plan

You do not need fifty systems. You need this one cold and the framework automatic, so the next "design X" is just new nouns in the same five steps.

  • Days 1–2: Learn the five-step framework until it is reflex. Do the proximity service end to end on paper — requirements, estimate, API, data model, and the search-service / geo-index / metadata boxes.
  • Days 3–4: Drill the geospatial index: why a scan and a plain lat/long index both fail, how a geohash prefix encodes a cell, how a quadtree subdivides, and what S2 adds — and say in one sentence when you'd pick each.
  • Days 5–6: Own the query flow and density — covering cells plus neighbours, the exact haversine refine, ranking and pagination, and the fixed-grid-vs-quadtree density trade-off plus caching and read replicas. Answer each of the five follow-ups out loud.
  • Day 7: A full timed 45-minute round, narrating the whole way, with someone (or something) interrupting. This is the rep that counts.

This post is the deep walkthrough behind one row of the broader Top 25 system design interview questions — once the proximity service is automatic, the same spine carries the rest of the list.

Frequently asked questions

How do I design Yelp step by step?

Follow the five-step HLD framework. Clarify requirements (search businesses within a radius of a location and get them ranked with name, category, rating and distance; open a business for details; add and edit businesses; the system is read-heavy and business data changes slowly); estimate to show that the load is overwhelmingly reads against tens of millions of mostly static businesses with wildly uneven density; define the small API (search_nearby, get_business, add_or_update_business, a viewport variant) over two layers — a spatial index and a business-metadata store; draw the design (clients → API → search service that computes covering cells, reads candidates from a geospatial index, refines by exact distance, ranks and paginates, backed by read replicas and a cache); then deep-dive the geospatial index (geohash vs quadtree vs S2), the query flow (covering cells plus neighbours then haversine), and density plus read-scaling. Finish on the follow-ups about boundaries, density and read-heaviness.

How do you find nearby places without scanning every business?

By giving every business a spatial cell key so "near me" becomes an indexed cell lookup instead of a scan. A full scan computes a distance for every row on every search, which is impossible at tens of millions of businesses, and a plain B-tree on latitude and longitude barely helps because two separate 1D indexes do not compose into a 2D range — each leaves a planet-long strip to scan. Instead you encode each location into a geohash prefix (or a quadtree node, or an S2 cell id) so nearby points share a key, turn a radius into the small set of cells that cover it, read only the businesses in those cells, and then refine with exact distance. You go from fifty million rows to a few hundred candidates.

What is a geohash?

A geohash is a way of encoding a latitude/longitude pair into a short string so that nearby points share a common prefix. You interleave the bits of the latitude and longitude and base-32 encode the result; the longer the string, the smaller and more precise the cell it names. Because nearness on the map becomes a shared string prefix, a geohash works with any ordinary string index — a plain database column or a key in Redis — and a "find nearby" query becomes "the handful of geohash cells that cover this circle." Its limitation is that it is a fixed grid: every cell at a given precision is the same size regardless of how many businesses fall inside it, which is why uneven density pushes designs toward a quadtree.

Geohash vs quadtree vs S2 — what's the trade-off?

All three turn two-dimensional proximity into a one-dimensional indexable key; they differ in how they handle density and where they live. A geohash is a fixed grid of equal cells encoded as prefix strings — simplest and portable, it runs on a plain index, but a uniform cell size over-returns in dense areas and wastes space in sparse ones. A quadtree recursively splits each square into four until a cell holds at most N points, so it adapts to density — dense regions subdivide deeply, sparse regions stay coarse — at the cost of maintaining an in-memory tree. Google S2 numbers cells along a Hilbert curve so each map cell is a 64-bit id with nearby cells sharing nearby ids, handling the whole globe cleanly for planet-scale systems. Pick geohash for simplicity, a quadtree for uneven density, S2 at global scale.

How do you handle uneven density and a read-heavy load?

Density is the real trade-off. A fixed-grid geohash uses one cell size everywhere, so a downtown cell can hold ten thousand businesses while a rural cell holds two — the dense cell returns far too many candidates and the sparse one too few. A quadtree solves this by subdividing only where it is dense and leaving empty regions coarse, so each cell holds a bounded number of businesses; alternatively you tune geohash precision per area to approximate the same effect. The load itself is overwhelmingly reads against slowly changing data, so you cache popular searches and hot cell-to-businesses results, serve business metadata from read replicas, and keep the geo index separate from the metadata store so each scales on its own and a rare write never becomes a bottleneck.

How do I practise a system design interview out loud?

Reading a walkthrough is not the same as defending a design while someone interrupts. On Zynter, the HLD track runs live voice rounds modelled on how real system design loops run — you talk through a shared design on the canvas, and a voice interviewer pushes with the follow-ups above one at a time, then gives you a debrief on problem solving, communication, technical execution, and time management. The score is a practice signal, not a hiring decision.

Practise it on Zynter

You can read what a geohash is. The round is won on turning a radius into cells out loud.

Pick the Local Business Search (proximity) round, draw the search-service, geo-index and metadata design on the canvas, and defend it out loud to a voice interviewer that keeps asking "find every restaurant within a kilometre without scanning fifty million rows — how?" and "one cell has ten thousand places and another has two — now what?" — then read the debrief. Zynter.ai is on-demand mock interviews modelled on real company loops, with 150+ rounds across 20 companies including the HLD system design track (Sep 2026).

Practise the Proximity Search round →
Browse all interview tracks

Top comments (0)