Design complete systems / CHAPTER 60
Design nearby places and friends (Yelp)
Find public places inside a requested radius; for moving friends, also check the latest coordinates, report age and sharing permission.
Builds on Database indexes, Data partitioning, Strong versus eventual consistency.
- Explain how a spatial index narrows a geographic search.
- Include regions across a cell border, then enforce the exact requested radius.
- Give moving friends separate freshness and privacy rules from public places.
01Functional requirements
Nearby search answers a geometric question: which places are inside a circle around the requested centre? Public cafés change slowly; a friend’s reported position can change every few seconds and is private. We start with public places, then add the friend extension.
FR1: Radius search. Find public places within a validated, bounded radius of the query centre. A spatial index supplies possible matches, called candidates. Membership in a candidate cell is only a shortcut; exact geographic distance decides whether the place qualifies.
FR2: Category filtering. Filter qualifying places by a supported category, such as cafés, rather than treating every nearby object as a match.
FR3: Distance-ordered pages. Return qualifying places in distance order with a stable ID tie-break and a cursor bound to the centre, radius, filters, and traversal context.
FR4: Place details. Return the current display details for a selected public place.
FR5: Owner-managed places. Let authenticated place owners create or update their records, with abuse controls and version checks for concurrent edits.
FR6: Friend location reports. In the consenting-friend extension, accept a user’s latest location report with a verified session, a sequence number that increases within that session, and the time the position was observed. Reject reports that arrive out of order or are too old.
FR7: Private friend discovery. Show nearby friends only to viewers permitted by the current sharing rule. A user can disable or change sharing; every friend query uses private candidate discovery and current consent checks rather than a shared public-place response.
Out of scope
- Ratings and general free-text relevance ranking.
- Proof of a person’s physical presence. A recent coordinate report is evidence of what a client reported, not proof that the person is there.
- Recalling information already displayed on another device after sharing is revoked.
Example. Mina requests cafés within 1,000 metres. A is 600 metres away across a cell border, B is 1,200 metres away in a candidate cell, and C is 800 metres away. The distance-ordered result is A, then C; B must be excluded.
02Nonfunctional requirements
These are independently chosen exercise assumptions to validate, not measured production guarantees.
NFR1: Place-query latency. At least 95% of measured place queries must finish within 200 ms at the assumed peak of about 2,310 searches/second. Measure receipt to completed response, including finding candidates, reading current records and applying exact distance.
NFR2: Place-index freshness. An ordinary accepted place edit must become searchable within one minute. Measure from saving the edit in the main place database to the search index including that version.
NFR3: Friend-report freshness. Return no friend report older than 30 seconds under the documented policy for observation time, server acceptance time and tolerated clock error. Enforce age on every read, even if cleanup has not run. Uploading an old observation now must not make it fresh.
NFR4: Exact place geometry. For a fixed searchable place-index version, exact mode must cover every intersecting region and exclude points beyond the radius. An arbitrary candidate cap that can miss a nearer qualifying place requires an explicitly approximate response contract.
NFR5: Moving-index completeness boundary. Cell-list updates run after the latest position is saved. A query can temporarily miss a moved friend before the new cell lists that user. Measure and disclose that delay. Checking current positions removes old candidates but cannot discover an ID missing from the searched list.
NFR6: Current sharing correctness. Return zero friends whose current sharing rule denies the viewer. Missing permission evidence suppresses the friend, and precise private locations must not be placed in a reusable public-place cache.
NFR7: Location-data protection. Restrict access to raw reports, retain them only for a declared short-lived product purpose, reduce displayed precision when permitted, and rate-limit repeated location queries. Define the actual retention duration before launch; the 30-second display cutoff is not itself a deletion policy.
03Back-of-the-envelope Estimates
Public places and moving friends have very different update rates. Estimate them separately before giving both the same storage path.
- Place searches. With 20 million searches/day, average rate is
20,000,000 ÷86,400 ≈231/second. An assumed tenfold peak is 2,310/second. Checking every one of ten million places per request would repeat ten million distance calculations, so use a spatial index to eliminate distant regions first. - Search responses. Twenty places at 500 bytes each make 10 KB/search.
20 million ×10 KB =200 GB/daybefore images and protocol overhead. Serve place photos through object storage/CDN rather than returning image bytes from the query database. - Place records. Ten million places at 1 KB each require 10 GB raw, before indexes and copies. A frequently used 100,000-record subset is 100 MB raw. A spatial database can be a sensible starting point; the record count alone does not justify a custom global grid.
- Friend reports. One million active users reporting every ten seconds send
1,000,000 ÷10 =100,000 updates/second. At 100 bytes each, ingress is 10 MB/second. This path needs much more update capacity than occasional place edits. - Retention choice. Keeping every report produces
10 MB/second ×86,400 =864 GB/day. Keeping one latest 100-byte record per million users is about 100 MB raw, plus expiry and index state. Retain history only for a defined product purpose rather than treating it as free. - Busy regions. A single cell containing 100,000 places can still make queries expensive. Measure how many candidates the handler examines for each returned place, by region and radius, before choosing cell sizes or an approximation policy.
04API Design and Retry Contracts
Mina’s browser asks the place-query handler for cafés inside a circle. The example response names A and C, the same places used in the metre sketch; B is excluded because it lies outside the radius.
GET /places?lat=40.70&lon=-74.00&radiusM=1000&category=cafe&limit=20
{"radiusM":1000,"places":[{"placeId":"A","distanceM":600},{"placeId":"C","distanceM":800}],"nextCursor":null}
- Validate a place search.
GET /placestakes a centre, radius, category, limit and optional cursor. Check coordinate ranges, cap radius and result count, and return400for invalid input. The response includes IDs, display fields, calculated distances and the next cursor. - Continue the same query. Bind the cursor to the centre, radius, category and ordering context. A distance from one centre is not the same sort key for a different centre, so a caller cannot reuse a cursor after changing those inputs.
- Read or edit a place.
GET /places/{id}returns current details.POST /placescreates an owner-managed record;PATCH /places/{id}requires that owner and the expected record version. Reject a stale version rather than overwrite another edit. - Report a friend’s position. Authenticated
POST /locationstakes the active session ID, increasing report sequence, observation time and coordinates. Return server acceptance time. Session and sequence checks stop an old upload from replacing the user’s newer position. - Find friends or change sharing.
GET /friends/nearbyuses the logged-in viewer, not a viewer ID supplied by the caller.PATCH /sharingchanges the user’s enabled state or audience with an expected version. Every friend response checks those current rules; a stale cell entry does not grant access.
05Data Model and Access Paths
The place database stores public records. For friends, keep the latest report and sharing decision separate from the search index: one record says where the user last reported being, and another says who may see it.
| Record / index | Key fields | Authority or shortcut |
|---|---|---|
Place placeId |
Coordinates, category, display fields, owner, version | Authoritative stable data |
| Spatial index / cell membership | Region/cell, object ID, position version | Candidate lookup |
Latest location userId |
Session, sequence, coordinates, acceptedAt, observedAt |
Latest accepted report |
Sharing rule userId |
Enabled, allowed audience, version | Current permission |
- Place-query code reads spatial-index candidates, then obtains the records needed for category, distance and display checks.
- Location-ingest code compares an arriving report with the current session and sequence in the user’s latest-location record. A sequence is an increasing number within that session. If 12 is already stored, delayed 11 cannot replace it.
- Membership work records the old and new cells with the accepted report version. A background program updates those cell-to-user lists; they are search shortcuts, not the main position record.
- Friend-query code checks current sharing, report age and latest coordinates before returning a candidate. It uses server acceptance and validated observation time under the documented clock-tolerance policy. Uploading an old observation now must not make it fresh.
- Cleanup code removes obsolete reports and memberships according to retention. The response still enforces the 30-second display cutoff if cleanup is late.
06Baseline: Cover the circle, then filter exactly
Start with public cafés in one database that supports indexed geographic distance queries. A spatial index groups points into regions so the database can skip most distant records.
- Mina’s browser sends the centre, category café and radius 1,000 metres to the place-query handler.
- The handler validates them and asks the database for points within that geographic distance. The index first finds regions intersecting the circle. Those regions supply possible matches, called candidates.
- The database or query handler calculates exact geographic distance for those candidates and applies the category filter.
Ais 600 metres away across a cell border, so it must be considered.Bbelongs to a searched region but is 1,200 metres away, so it is rejected. - The handler sorts qualifying records by
(distance,placeId)and returnsAat 600 metres, thenCat 800 metres. The stable ID breaks equal-distance ties. - The next-page cursor keeps the same centre, filters and traversal view. The handler cannot silently switch query geometry midway through the result.
The broad index check and exact distance check solve different problems. Reading only the centre cell can miss A; returning every member of a searched cell can wrongly include B. Use geographic types and tested distance operators. A degree of longitude changes its metre width with latitude, so flat degree arithmetic is unsafe.
| Index approach | What the database searches | Added cost or limit |
|---|---|---|
| R-tree-style index | Nested bounding regions that may overlap | More overlapping regions can mean more candidates |
| Quadtree | Regions repeatedly divided into four smaller parts | More nodes and traversal in divided regions |
| Fixed or hierarchical cells | Cells assigned from coordinates | Cell size and border covering affect work |
A geohash is a string encoding nested cells; longer prefixes describe smaller areas. Nearby points can sit on opposite prefix borders. The string is useful for indexing, but the handler still needs complete region covering and exact distance.
A radius crosses cell borders; cell membership is only a first filter
Trace the diagram in words
- Cover every cell intersecting the 1,000 metre circle.
- A is 600 metres and C is 800 metres from the centre, so both qualify.
- B is 1,200 metres away and is rejected even though it belongs to a covered candidate cell.
BUILD THE DESIGN, ONE CHANGE AT A TIME
Watch the architecture evolve.
Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.
Stage 1 of 3
Start here. First make one indexed database return the correct cafés inside Mina’s circle.
Spatial database
Follow the numbered arrows
- Nearby request → Spatial query serviceone fixed centre and radius
- Spatial query service → Place/position recordsindex-aware candidate query
- Place/position records → Exact serving filterpoints with current metadata
- Exact serving filter → Nearby requestdistance-ordered qualifying page
Trace the diagram in words
- Mina’s handler validates the 1,000-metre café query.
- The index searches intersecting regions, including A’s neighboring cell.
- Exact distance retains A at 600 metres and C at 800 metres and rejects B at 1,200 metres.
- The query handler validates centre, radius and category.
- The spatial index supplies candidates; exact distance removes out-of-radius points.
- The handler sorts qualifying places by distance and ID and preserves the query in its cursor.
The cost: One database has a capacity limit and needs recovery copies. Its distance operator must handle the date line and changing geographic scale.
Why change it? If busy-city queries miss the 200 ms target or update traffic saturates the spatial database after local capacity tuning, distribute cell lookups while retaining the same exact-distance check.
Partitioned place candidates
Follow the numbered arrows
- Nearby request → Spatial query serviceone fixed centre and radius
- Exact serving filter → Nearby requestdistance-ordered qualifying page
- Spatial query service → Public candidate cachepublic places only: cached candidate lookup
- Spatial query service → Circle coveringcache miss:cover the full circle
- Circle covering → Partitioned cell indexquery every intersecting region
- Partitioned cell index → Exact serving filtercandidate IDs
- Place/position records → Exact serving filterauthoritative coordinates and fields
- Public candidate cache → Exact serving filterpublic-place hit:IDs still need exact filtering
Trace the diagram in words
- The handler covers all intersecting cells and sends lookups to their owners.
- It merges and removes duplicate public IDs, then reads records needed for filtering.
- It applies the same exact test after either a cache hit or a direct index lookup.
- The handler computes every cell intersecting the circle and queries their storage owners.
- Public candidate caches save repeated lookups without storing private friend responses.
- The handler still reads needed records and applies the same exact distance and category tests.
Inspect the connection changes
- Now: Spatial query service → Public candidate cache: public places only: cached candidate lookup
- Now: Spatial query service → Circle covering: cache miss:cover the full circle
- Now: Circle covering → Partitioned cell index: query every intersecting region
- Now: Partitioned cell index → Exact serving filter: candidate IDs
- Now: Place/position records → Exact serving filter: authoritative coordinates and fields
- Now: Public candidate cache → Exact serving filter: public-place hit:IDs still need exact filtering
- Previously: Spatial query service → Place/position records: index-aware candidate query
- Previously: Place/position records → Exact serving filter: points with current metadata
All other connections are retained. Response paths are implicit unless drawn.
The cost: Smaller cells reduce irrelevant candidates but require more requests. Exact results still need every required region; cache hits cannot justify missing regions.
Why change it? The example’s 100,000 friend reports/second need a separate update path, and users can move or stop sharing before cell work finishes.
Moving private friends
Follow the numbered arrows
- Nearby request → Spatial query serviceone fixed centre and radius
- Exact serving filter → Nearby requestdistance-ordered qualifying page
- Spatial query service → Public candidate cachepublic places only: cached candidate lookup
- Spatial query service → Circle coveringplace cache miss / every friend query
- Circle covering → Partitioned cell indexquery every intersecting region
- Partitioned cell index → Exact serving filtercandidate IDs
- Place/position records → Exact serving filterauthoritative coordinates and fields
- Public candidate cache → Exact serving filterpublic-place hit:IDs still need exact filtering
- Moving-user reports → Place/position recordsaccept only valid newer report
- Place/position records → Partitioned cell indexversioned old/new membership work
- Sharing authority → Exact serving filtercurrent sharing rule per viewer
Trace the diagram in words
- The query handler chooses the private index, never the public-place cache, for friends.
- Ingest code stores Jo’s newer sequence and rejects a delayed older report.
- The handler removes repeated IDs and checks current coordinates, age and sharing before return.
- Location-ingest code saves a newer session report and required old-cell/new-cell work.
- Background membership updates may temporarily duplicate or omit a user.
- Friend queries bypass public caches and check latest position, age and current sharing before returning a person.
Inspect the connection changes
- Now: Spatial query service → Circle covering: place cache miss / every friend query
- Now: Moving-user reports → Place/position records: accept only valid newer report
- Now: Place/position records → Partitioned cell index: versioned old/new membership work
- Now: Sharing authority → Exact serving filter: current sharing rule per viewer
- Previously: Spatial query service → Circle covering: cache miss:cover the full circle
All other connections are retained. Response paths are implicit unless drawn.
The cost: Read checks can reject an old membership but cannot recover a missing new-cell ID. A complete immediately updated friend view needs a stronger update/query protocol.
07Evolution: Handle skew and moving objects
First measure the spatial database and consider a larger machine that can keep more indexes in memory. Add explicit cell routing only when measured query or moving-update work requires it.
Divide public-place retrieval across machines
- Assign geographic cell ranges to storage machines. For Mina’s circle, the query handler computes every intersecting cell and sends lookup requests to the responsible machines.
- Each machine returns candidate IDs. The handler combines IDs, removes duplicates and loads the records needed for exact distance and category checks.
- Keep A and
C, rejectBand sort by distance/ID as before. For an exact result over a chosen searchable index version, no intersecting region may be silently omitted. Ordinary place edits still follow the agreed one-minute indexing target. - Cache reusable public candidate IDs or immutable place versions. A cache hit does not replace exact filtering. Replicate busy public regions when measured load justifies the extra copies.
Small cells remove more irrelevant points but require more cell requests for a large circle. Large cells need fewer lookups but can contain many irrelevant city points. Adaptive subdivisions can help dense cells. If a work cap can miss a nearer place, label the response approximate instead of promising the exact nearest twenty.
Add the moving-friend path
- Location-ingest code accepts Jo’s new report only if its session is current and its sequence is newer than the saved one. It saves the latest position and required old-cell/new-cell work.
- A background program removes Jo from the old cell and adds Jo to the new cell. It can retry those versioned operations after a crash. Until they finish, Jo can appear twice or be absent from the new cell.
- The friend-query handler bypasses the public-place cache. It covers every intersecting private-index region and removes repeated user IDs.
- For each candidate, it reads the latest report and current sharing rule, checks age and exact distance, then returns only permitted friends. An old membership can be rejected safely; a missing new-cell ID cannot be discovered by checking only the IDs already found.
This friend index therefore has an explicit temporary-omission limit. A stronger complete live view would require a different coordinated update/query contract, not merely another read-time check.
See stage 2: Partitioned place candidates ↗See stage 3: Moving private friends ↗08Failure and Recovery Scenarios
Use Jo’s move from cell W to E to distinguish the checks:
- A report arrives out of order. The main latest-location record already has sequence 12. Ingest code compares the session and sequence and rejects 11; arrival time does not make 11 newer.
- Old membership remains.
Wstill lists Jo after sequence 12 moved Jo toE. AWquery reads the latest coordinates and rejects Jo if the exact distance is now outside its circle. - New membership is delayed.
Edoes not yet list Jo. Its query can miss Jo entirely. Checking current records removes stale candidates but cannot find an absent ID; measure this indexing delay. - Sharing changes. Jo disables sharing while both memberships remain. Before return, the handler reads the current sharing record and omits Jo. If that check is unavailable, omit the friend rather than reuse old permission.
- The report becomes old. The handler checks the 30-second age rule on every response. A slow cleanup worker cannot make an expired report eligible, and a late upload cannot refresh an old observation’s age.
- The circle crosses unusual geography. Test cell borders, the date line and poles. Longitudes near +180 and −180 can be close on Earth; tested covering and distance tools must handle that wrap.
Restrict access to precise reports, keep them only for the declared purpose and reduce displayed precision where allowed. Rate-limit repeated location queries that could reconstruct movement. Revoking sharing governs later responses; it cannot erase information already shown on another device.
09Trade-offs and Validation
Choose an index by the work the handler actually performs.
- Stay with a spatial database when indexed distance and category queries meet the measured budget. It keeps public updates and queries in a simpler system.
- Add a cell service when measured region load or moving-report volume justifies routing, versioned memberships and repair. A global audience alone is not the trigger.
- State exactness separately: exact public results need complete region coverage and a consistent traversal; approximate suggestions may cap work only under a stated approximation contract.
- State private freshness separately: a smooth map animation does not prove the reported coordinates are recent, permitted or fully indexed.
Measure candidates examined per result, index delay, report age, rejected old reports, permission-check failures and query latency by radius and region. Close with the three cafés: include A across the border, exclude B by exact distance and return A/C in order. Then explain which current record excludes Jo after a move, expiry or sharing change.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
A model can optionally turn a sentence into proposed public-place filters. It does not decide geographic eligibility or friend permission.
- Input and output. For “quiet cafés within a kilometre,” the model proposes supported categories, tags and radius. The application validates those fields, shows its interpretation and allows correction. Apply “quiet” only if approved catalog data establishes that tag for the place; otherwise show it as unsupported.
- Query integration. After validation, the ordinary place handler covers the search region, checks exact distance and returns its ordered page. A model cannot make an out-of-radius café qualify or discover a missing friend-index entry. Keep private friend coordinates and sharing relationships out of this parser.
- Fallback and timing. Assume a150 ms parsing budget, measured separately from the 200 ms spatial-query target. If the model times out or is unsure, offer the usual category and radius controls.
- Evaluation and privacy. Measure interpretation accuracy, invalid filters, empty-result regressions, latency and cost per interpreted query across languages. Minimize retained text and precise-location context. Any meaning-based extension uses permitted public metadata; current friend consent still comes from the sharing record.
The exact query works without AI.
DRAW. CALCULATE. EXPLAIN.
Prove a nearby query across a cell boundary
75 minutes · guided practice
Draw four adjacent cells with a query circle crossing their shared corner. Place one in-range object in each cell and two out-of-range objects inside candidate cells. Trace the query. Then move one friend to another cell, delay index cleanup, and disable sharing. State exactly what the viewer receives at each step.
Compare the worked solution
- Collect candidates from every intersecting cell. The circle crosses four cells, so the handler asks all four rather than only the one containing the centre.
- Apply the exact test. Calculate distance for each candidate, remove the two outside points and apply the category filter. Sort survivors by distance and stable ID. Keep the same centre and filters for subsequent pages.
- Read the moved friend’s current record. Remove repeated user IDs if both cells list the friend. Read the latest accepted session, sequence, coordinates and age. An old cell entry cannot force an old position into the answer.
- Expose the omission limit. If the new-cell insert is delayed, a query there can miss the friend. A current-record check cannot recover a candidate ID that the search never found.
- Enforce sharing now. Once sharing is disabled, omit the friend even if both memberships remain. If current permission is unavailable, also omit it. Cleanup reduces old entries; it does not decide whether the response is allowed.
- Rehearse the café example.
Aat 600 metres andCat 800 metres pass the 1,000-metre test in that order.Bat 1,200 metres fails even though its cell was searched. Define retention and permitted display precision separately from this geometry.
The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.
RETRIEVE BEFORE YOU REVEAL
Keep the important ideas.
Say the answer aloud, then turn the card over.
CARD 01What are the two stages of a nearby query?
The handler first asks every indexed region intersecting the circle for candidate IDs. It then calculates exact distance and applies filters. A across a cell border can qualify; B inside a searched cell can still lie outside the requested radius.
CARD 02When should a spatial database become a custom cell service?
Start with a database that supports indexed geographic queries. Add cell routing when measured query or update load exceeds that database’s tested capacity. Smaller cells reject more distant candidates early but require more cell requests for a large search circle.
CARD 03What must a nearby-friend response verify?
For each friend candidate, read the latest report and current sharing rule. Check session, sequence, age and exact distance before return. These checks reject old entries but cannot discover a friend missing from the new cell index.
CHECK THE REASONING
Can you explain the choice?
Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.
CLOSE THE LOOP
Explain it without the page.
Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.