Finding coffee around the corner should not mean reading every place in the world. The design has to narrow the search by location without dropping a café that sits just across an index or shard boundary.
This walkthrough uses a spatial database with regional read replicas. That is one strong answer; an in-memory index of grid cells also works. Scale figures come from the question's brief, and the latency and freshness budgets are assumptions.
Clarify what nearby means
People search around a latitude and longitude, choose a radius, optionally filter by category, and open a place's details. Owners add places and update addresses, coordinates, hours and phone numbers. Reviews, routing, photos and personalised ranking stay out of scope.
Assume results are sorted by geographic distance, with place ID breaking ties. "Nearest" means straight-line distance over the earth, not walking time. A café across a river might rank ahead of one with a shorter walk. Directions would be a different product requirement.
The brief gives 200 million places, 500 million searches a day and a peak of about 20,000 searches a second, against an average of roughly 5,800. Owners make about 100,000 place changes a day, which averages about one a second, though updates can cluster. Searches outnumber changes by about 5,000 to one, so the read and write paths can scale separately.
Aim for search responses under about 200 ms and new listings appearing within a few minutes. Slightly stale results are acceptable during failures. Most radii are between 500 metres and five kilometres. Cap the supported radius and page size so one request cannot ask for an unbounded area or result list.
Model a place and its public read view
The source record holds a place ID, owner ID, name, category, coordinates, address, hours, phone number, version and deletion state. A location is a geographic point, not an address string. Turning addresses into coordinates happens upstream and is outside this walkthrough.
Keep compact search fields apart from heavier details. The spatial search needs ID, coordinates, category, name and a short address; a details page can fetch the complete record. Store coordinates with an explicit reference system and validate latitude and longitude ranges.
The conceptual API is:
GET /places/nearby?lat=...&lng=...&radius_m=...&category=...returns a bounded page with distances and a continuation token.GET /places/{id}returns the current public details or a missing/deleted response.POST /placesandPATCH /places/{id}save an owner's listing and return its version.
Take the owner from the authenticated session and check authorisation on updates. Send the expected version with each edit if two concurrent owner changes must not silently overwrite one another.
Use a spatial index to reduce candidates
This design stores places in PostgreSQL with PostGIS, as a geography point with a spatial index. An indexed radius predicate finds candidates, the query applies the category filter, calculates distance for the rows that qualify, and returns the nearest page.
PostGIS's ST_DWithin takes distances in metres for geography values and includes an index-aware bounding-box comparison. The pattern is a cheap index check that rules out distant places, then an exact distance test on what remains. Sorting the whole places table by distance on every request is what the index is there to avoid.
A plain latitude and longitude rectangle is not enough as the final filter. Its corners include places outside the requested circle. Units matter too: one degree is not a fixed number of metres in every direction or at every latitude.
For a one-kilometre coffee search, the spatial index limits the rows to the area, the radius predicate rejects points outside the circle, and the category filter removes places that do not serve coffee. Return the closest 20 plus a cursor. Measure the actual query plan against real density and category distribution, because having an index does not prove the latency target.
Cover the whole search circle, including shard edges
Partition data into broad geographic regions when capacity requires it. The router finds every region the search circle intersects, queries their read replicas in parallel, removes duplicates by place ID, and merges by distance and ID. A search near a border must query both sides.
This applies to small radii too. A café one metre across a shard boundary must still appear. Test boundary cases, the international date line and high latitudes instead of assuming a flat coordinate calculation works everywhere.
A grid-based alternative must read every cell the circle intersects, then apply the same exact-distance filter. "Current cell plus eight neighbours" is only safe if the cell size, latitude and radius are chosen so the circle cannot reach past those neighbours. A larger radius breaks it. PostGIS keeps that cell rule out of the application, though the database still needs an efficient spatial query plan.
Scale searches separately from owner writes
Place changes commit to an authoritative database shard, and replication copies the records and their index state to read replicas. The spatial index lives inside the database, so there is no separate write to a search index that could succeed or fail on its own.
Add search-service instances and read replicas to handle read demand. Regional replicas cut network distance for local searches, but each copy costs storage and adds replication lag. Serve detail reads from a cache, falling back to the authoritative database on a miss. A cache hit should not need a database query to check its version.
If the same areas and categories are searched repeatedly, cache candidate IDs for them for a short time. A cached answer for one centre point is not correct for every point in that area. Either cache a broad enough candidate set and recalculate distance from the caller's actual coordinates, or key complete responses by the exact search parameters. If you round coordinates for the cache key, say how that changes the results a caller gets.
Measure how long a change takes to appear
An owner should see the saved record straight away from the write response, even while public search replicas catch up. Track replication lag per region and alert before it exceeds the freshness budget. If one replica falls behind, route searches to a healthy copy rather than hide new listings for hours.
Invalidate cached details after a successful update. An older read that missed the cache can still fill it after the invalidation. A short TTL bounds that staleness, and version-aware cache writes can close the gap. Cache TTL, replica lag and search-result TTL all add to how long a change stays invisible.
When a place moves between geographic shards, keep its ID and coordinate the transfer through a durable workflow. Copy the new version to the destination shard, mark the old version retired, and let searches drop duplicate copies during the move. Without that workflow, a successful save could leave the business searchable only at its old address.
A separate search engine or cell index would need its own reliable update pipeline: a transactional outbox or change stream, a consumer that can replay it, and version checks. Writing the database and then updating the index on a best-effort basis is not enough. A separate index is worth its operating cost when you need specialised ranking or a different storage layout.
Dense city centres and partial failures
A five-kilometre search in central Tokyo can match far more candidates than one in a rural area. Measure rows examined, geographic density, sort cost and tail latency. Smaller partitions, suitable category indexes and a better nearest-neighbour query plan may help. Returning 20 results limits response size, but the query may still inspect many more than 20 rows.
For pagination, use an opaque cursor bound to the original coordinates, radius, filter, and the last distance and ID. Concurrent edits can still shift ordering between pages. Accept small pagination drift for a city guide; a consistent snapshot would need a stronger and more expensive contract.
If an authoritative database is down for a while, replicas and caches can keep serving slightly stale reads. Owner edits should fail clearly instead of claiming to be saved. If one geographic shard cannot answer, do not return a partial result as if it were the complete nearest list. Return an explicit partial-results state or fail the search, depending on the product policy you chose.
Follow a search across a shard boundary
Try nearby places search in Grind. Talk through a coffee search at a region boundary, then a business moving to a new address. Explain which records each request touches, where the exact distance check happens, and how you know a saved edit reaches search soon enough.


