System design interview guide
Nearby Friends System Design Interview
A user opens a map at a concert and sees "three friends nearby." One friend paused sharing. Another moved across town ten minutes ago. A bad actor keeps querying the same person every few seconds. The design must feel fresh without draining batteries, and it must protect privacy as seriously as it handles geospatial search.

Problem statement
You're designing a nearby friends feature: opt-in users share fresh, coarse location; viewers search within a radius; the system filters by friend graph, blocks, visibility, and freshness; and privacy controls are part of the core design rather than a checkbox after the geospatial index.
Introduction
Nearby friends feels like a simple map feature until you imagine the wrong dot at the wrong time. A friend who paused sharing should not appear because an old index row survived. A person who left a concert thirty minutes ago should not still look "nearby." A repeated query pattern should not let someone track a route home.
This page is in the system design interview guides hub.
If this is your first time, go to Board path below—come back to this intro after one mock.
Nearby friends (interview definition): Not “find every stranger in the city.” Opted-in friends share a short-lived location. When someone opens the map, you: look in a small local area → keep only allowed friends → drop old or hidden locations → show coarse distance plus last_seen (not exact GPS by default).
The real design is privacy-sensitive and battery-limited. You are answering: among people I am allowed to see, who has a fresh location near this radius?
The safe mental model is: write sparingly, store briefly, search a small local area, filter by friendship and privacy, then return coarse results.
Picture the map as a grid of cells (buckets on the earth). A cell answer is only “who might be nearby”—not “who is allowed to appear.” Permission rules and a freshness window decide that. You’ll name a cell library later, after the board is clear.
Weak answers check exact distance to every friend (or every user) on every map open, or store exact GPS forever “for analytics.” Strong answers use cells to shrink who to check, cap how old a location can be, enforce opt-in and block on every path, and say what happens when location is stale or uncertain.
Board path (first read, ~10 min)
Start here if you have never designed nearby friends. Finish this section, then stop and try a short requirements mock. Cell library names, crowded-venue ops, and multi-region splitting live in After one mock.
What problem are we solving?
A user opens a map and asks:
“Among people I’m allowed to see, who has a fresh location near me?”
Wrong answers look like: a friend who paused still appears; someone who left 30 minutes ago still looks “nearby”; a creepy pattern of repeated checks tracks someone home.
You are not building “find every stranger in the city.”
What to write on the board (start simple)
- Who can share? — Opt-in. Pause/hide must hide them.
- Who can see whom? — Friends + blocks + visibility rules on every query.
- How does a phone report location? — Only after opt-in, and only when movement or time justifies it (not every GPS tick).
- How do we search? — Look in a small local area → keep only allowed friends → drop stale → show coarse distance (“within 500 m”), not exact GPS by default.
- What if we’re unsure? — Hide. Empty is safer than a wrong or unauthorized dot.
That’s the first board. Cell brand names come after this mock.
Clarifying questions (say these in the room)
You: “Is this opt-in for both sharing and viewing, and do friends need mutual permission to see each other?”
Interviewer: “Yes, users opt in; friends and blocks matter.”
That answer makes privacy state part of the hot query path, not a settings page you can ignore.
You: “What precision should we show: exact pin, distance in meters, or coarse buckets like ‘nearby’ or ‘within 500 m’?”
Interviewer: “Use coarse buckets by default; exact location is not required.”
Checkpoint: Write on the board: “Default response = coarse bucket + last_seen, not raw lat/lng.” That choice drives storage precision and API shape.
You: “How fresh does a location need to be before we hide it or mark it stale?”
Interviewer: “Hide or mark stale after about 10 to 15 minutes.”
Checkpoint: Pick a freshness window (for example 10–15 minutes). Call it a TTL—time-to-live: after that, hide or mark stale so old dots don’t pretend to be live.
You: “How often can the phone send updates? Should we use every GPS tick or only movement and interval-based updates?”
Interviewer: “Be battery-aware; do not stream every GPS tick.”
Update on cell change, significant movement, or a minimum interval—not once per second.
You: “I want to keep turn-by-turn navigation, full location history analytics, and global people search out of scope so the round stays on nearby friends. Does that match what you want?”
Interviewer: “Yes, focus on friend-filtered nearby search and privacy.”
In the room (first-pass opening): “I’d make this opt-in and privacy-first. The phone sends location only when the user opted in and enough movement or time passed. We store a short-lived local area plus a timestamp—not permanent exact GPS by default. A nearby query looks in a small map area, keeps only allowed friends, drops stale rows, and returns coarse distance with last_seen.”
End of first pass
End of first pass — stop scrolling.
Practice clarifying questions and this board now. Do not continue into cell libraries, capacity math, or stadium scaling until you’ve done a short mock.
After one mock
Return to Board path if this is your first read.
Everything below is for after one mock—or when the interviewer pushes on cell indexes, hot venues, and multi-region scale.
How to approach
Clarifying questions for the requirements stage live in the Board path. After that mock, deepen with capacity, cell brand names, and one update / one query walkthrough.
Minute pacing (about 45-60 minutes)
| Minutes | Focus |
|---|---|
| 0-5 | Clarify opt-in, precision, freshness, update policy, and out of scope |
| 5-12 | Capacity and why updates can dominate reads |
| 12-25 | HLD: location update path, cell index, friend graph, query path |
| 25-35 | Data model and detailed read/write design |
| 35-45 | Architectural dig: moving users, stale cells, and hot events |
| 45-60 | Privacy, abuse, production scenes, bottlenecks, and interview tips |
If this is your first nearby-friends design, spend minutes 0–12 only on the Board path. Capacity tables and cell-library detail wait until after one mock.
In the room (a fuller opening you can actually say): "I will make this opt-in and privacy-first. The client sends location only when the user opted in and enough movement or time passed. The server stores a short-lived cell and timestamp, not permanent exact GPS by default. A nearby query computes the cells covering the radius, gets candidates from those cells, intersects them with the viewer's allowed friend set and blocks, drops stale rows, then returns coarse distance buckets with last_seen."
Out of scope (and why you park these)
Turn-by-turn navigation. Navigation needs continuous routing, road graphs, and live traffic. Nearby friends only needs approximate proximity to permitted people.
A permanent location-history warehouse. Analytics can be sampled or aggregated separately, but raw exact history creates privacy risk and does not belong in the core hot path.
Global people discovery. Searching strangers nearby is a different product with much higher abuse risk. This design is friend-filtered.
Perfect real-time precision. A few seconds or minutes of freshness is usually enough. Exact live tracking drains battery and increases stalking risk.
Capacity estimation
Location systems can surprise you because writes may be more expensive than reads. If every active phone sends GPS once a second, the backend and the battery both lose.
| Dimension | Rough scale | What this means for the design |
|---|---|---|
| Active sharers | Tens of millions globally | Most data should live near where people are; one global pile won’t stay healthy |
| Location updates | Can reach hundreds of thousands to millions per second if unthrottled | Client-side throttling and server admission are required |
| Friends per user | Hundreds typical, thousands at the high end | Friend intersection must be bounded and cached carefully |
| Hot cells | Stadiums, airports, campuses, festivals | The list of user ids in one cell (a posting list) can become huge; need caps or finer subcells |
| Freshness | 1-15 minutes depending on product | TTL and last_seen are part of correctness |
What this means for the design:
- Do not send every GPS tick — battery and write volume explode.
- Do not scan every user in a city — shrink to a local area, then to allowed friends.
- Never let stale or hidden locations look precise — prefer empty or “last seen 12 minutes ago” over a confident lie.
(How you split data across machines—by region or by map-bucket prefix—belongs after you can draw the happy path.)
High-level architecture
The design has four main services. Location ingress receives updates from opted-in clients and normalizes them into cells. Location index stores fresh user location by user id and by cell. Graph and privacy service owns friends, blocks, visibility, and sharing state. Nearby query service combines cells with graph rules and returns safe results.
Who owns what:
- Mobile client decides when to wake GPS, applies battery-aware throttling, and sends accuracy plus movement metadata.
- Location ingress authenticates the user, checks opt-in, rejects impossible jumps, rounds the location to the allowed precision (so you don’t store more detail than the product promises), and writes fresh state.
- Location index keeps
user_id -> current cell/locationandcell_id -> user idswith TTL. - Graph/privacy service returns allowed friend ids, blocks, close-friend tiers, and pause or hide state.
- Nearby query service computes covering cells, merges candidate lists, filters by friendship and privacy, calculates final distance, and returns coarse display fields.
- Abuse/risk pipeline watches repeated queries, suspicious target patterns, mock GPS signals, and internal tool access.
[ Mobile client ]
| opt-in + movement / interval update
v
[ Location ingress ] ---> [ Location index ]
| user -> cell + updated_at
| cell -> user ids (TTL)
|
+--> abuse / anomaly stream
[ Nearby query ]
| 1) auth + viewer privacy
| 2) graph: allowed friend ids + blocks
| 3) cells covering radius
v
[ Location index candidates ] -> intersect -> distance refine -> coarse response
In the room: Say the order clearly: cells find a local candidate set, graph and privacy decide who is allowed, exact distance runs only on survivors. Do not make “exact distance to everyone” the first step. (That exact-distance math is often called Haversine—you don’t lead with the name.) Include neighboring cells so friends near a cell border are not missed.
Core design approaches
Map buckets (cells) before brand names
Nearby search needs a cheap first filter: “Who is in this neighborhood of the map?” not “Check every user on earth.”
Divide the earth into cells—buckets with ids. On write, store “this user is in cell C.” On query, ask “which cells cover my radius?” and load only people in those cells. Then apply friendship, blocks, and freshness.
Geohash, S2, and H3 are common ways to create those cell ids. Geohash is easy to sketch (nearby places often share a prefix) but has awkward edges at cell borders. S2/H3 use hierarchical cells that are easier to tune when a stadium packs one coarse cell. In the interview, naming one and explaining the candidate-filter pattern matters more than arguing brands.
User-to-cell vs cell-to-users
You need two lookups:
- By user: “Where is Alex right now?” → store latest cell (and coarse location) for each sharing user.
- By place: “Who is in these nearby cells?” → for each cell, keep the list of people currently in that square (engineers often call this a posting list).
Nearby query needs the second. Latest-state updates need the first. Most designs keep both. If you only keep “where is each friend?” you still walk hundreds of friends one by one—fine for a tiny product, painful at high QPS.
Query cells first vs friends first
| Order | When it works | Cost risk |
|---|---|---|
| Friends first | Tiny friend graphs | Every map open walks all friends’ locations |
| Cells first, then friends | Large graphs, high QPS, dense venues | Spatial prune, then intersect allowed friends |
If a viewer has ~300 friends, either order can work for a small product. At scale, prefer cells first, then intersect. State the expected friend count and make the cost visible.
Pull map query vs push "friend nearby"
Pull is simpler: when a user opens the map, call nearby query. Push notifications like "friend nearby" are more sensitive because they can feel creepy and create notification storms at events. Default to pull, and treat push as an optional product with strict caps, quiet hours, and user controls.
Data model
Location data should be short-lived and permission-aware. The model needs to answer current position, cell membership, freshness, and visibility.
| Entity | Key fields | Why it exists |
|---|---|---|
location_state | user_id, cell_id, coarse lat/lng, accuracy, updated_at, version, TTL | Current fresh location for a sharing user |
cell_membership | cell_id, user ids, update version or score | Candidate lookup for nearby query |
privacy_state | user id, sharing mode, precision tier, paused flag, allowed audiences | Decides whether location may be returned |
friend_edge | user A, user B, state, close-friend tier | Friend filtering and optional precision tier |
block_edge | blocker, blocked, updated_at | Must remove visibility immediately |
query_audit | viewer, target or candidate count, time, reason, tool | Abuse and compliance review where required |
Use TTL on location state and cell membership so old locations disappear without a perfect cleanup job. Store raw coordinates only as long as needed for freshness and ranking. For display, return approximate distance buckets or coarse labels unless the product explicitly requires exact coordinates for a trusted tier.
Detailed design
Two journeys matter: a phone reports location, and a viewer asks who is nearby. Both must check privacy. A paused user’s update must not enter the index; a leftover cell row must not make them appear.
Write path (location update)
Say this out loud:
- Client sends only if opted in, and only after meaningful movement, a cell change, map open, or a minimum interval (not every GPS tick). Include accuracy; if accuracy is poor, delay or send a coarser cell.
- Server checks auth and sharing state (
POST /location:update). Paused/opted-out → reject or store only allowed coarse state. - Map the point into a cell id (bucket on the earth—geohash, S2, or H3).
- Write latest state (“user U is in cell C at time T”) and cell membership (“cell C contains U”), with a freshness expiry (TTL).
- If they left old cell O, remove or expire membership in O. A version number helps ignore late old writes.
- If the two views disagree briefly, the query trusts recent latest-state + timestamp—not a ghost membership.
A person walking a festival can cross cells often. Minimum intervals, movement thresholds, and coalescing (keep the latest of three quick moves) keep battery and write volume sane.
Read path (nearby query)
- Start with the viewer: may they use the feature?
- Load allowed friends and blocks (
GET /nearby/friends). - Take viewer location; list cells covering the radius. Include neighboring cells—a friend 10 m away can sit just across a square border; if you only open “your” square, you miss them.
- Fetch candidate user ids from those cells; merge and dedupe.
- Keep people who appear in both the local candidate list and the allowed-friends list (intersect). Drop paused, blocked, not visible, or older than the freshness window.
- Compute exact distance only for survivors; return coarse buckets +
last_seen, not raw GPS by default.
Example response shape (no lat/lng):
{ "friend_id": "u42", "approx_distance_bucket": "within_500m", "last_seen": "2m ago" }
In the room (spoken recap): “Client sends location only when opted in and movement or time justifies it. Server checks privacy, maps to a cell, writes latest state plus cell membership with TTL, and uses versions for late updates. Nearby query covers the radius with cells, fetches candidates, intersects friends and blocks, drops stale rows, computes distance for survivors, and returns coarse distance plus last_seen.”
Architectural dig
Moving users, stale cells, and hot events
The hard part is not turning latitude and longitude into a cell. The hard part is keeping moving, permissioned people from appearing in the wrong place.
1) Bad design: one exact row forever
user_id -> exact_lat, exact_lng, updated_at
Query:
load all friends
compute distance to every friend
return exact points
This looks simple, but it fails on privacy, freshness, and scale. It stores precise location too long, makes large friend sets expensive, misses spatial pruning, and returns exact points even when the product only needs coarse proximity.
2) Better design: versioned short-lived cell state
Example below uses an S2-style cell id. Treat s2:… as “bucket id,” not a library you must memorize.
location_state:user_123 = {
cell_id: s2:89c25,
coarse_lat_lng: ...,
updated_at: T,
version: 41,
expires_at: T + freshness_window
}
cell:s2:89c25 = sorted/set(user_ids with updated_at/version)
The latest-state row lets you verify freshness and privacy before returning a result. The cell membership lets you find nearby candidates cheaply. TTL makes old data disappear even if a cleanup worker misses a move.

Figure: A geo cell only finds candidates. Privacy and freshness checks decide what can leave the API.
3) Moving across cell boundaries
When a user moves from cell A to cell B, a naive system may leave them in both cells. That creates ghost dots. A robust system writes a newer version to location_state, adds membership to B, and removes or expires membership from A. Query results compare candidate version or updated_at against the latest-state row. If the membership is stale, the candidate is dropped.
4) Hot cells at events
A stadium can put thousands or millions of people into one coarse cell. Fetching the entire posting list for every map open is too expensive. Use adaptive resolution: split hot cells into subcells, cap candidate scans, or precompute close-friend subsets for users who often query. When the product can tolerate it, degrade to a coarse message such as "many friends in this area" rather than trying to rank every person in a packed venue.
5) Abuse inside the query path
Nearby friends can be abused by repeated queries against one person. Even if every query is technically authorized, the pattern may be unsafe. Rate-limit queries, audit sensitive access, detect repeated target lookups, and reduce precision when risk is high. Internal tools need the same or stronger audit than user-facing APIs.
Interview land: "A cell index finds candidates; it does not grant permission. Freshness, blocks, opt-in, and coarse display still decide what leaves the API."
Key challenges
| Challenge | What users see if it goes wrong | What to say in the interview |
|---|---|---|
| Battery drain | Phone gets hot; users disable the feature | Update on movement, cell change, or interval; use OS significant-change APIs |
| Stale dots | Friend shown nearby after leaving | TTL, last_seen, stale hiding, and version checks |
| Hot cells | Map query slow at concerts | Adaptive subcells, caps, deterministic ranking, and graceful coarse results |
| Privacy leaks | Exact home or commute exposed | Coarse buckets, opt-in, pause, short retention, audit |
| Block churn | Blocked user still sees location | Enforce blocks on query path and invalidate graph cache |
| Mock GPS or impossible jumps | User teleports across continents | Accuracy thresholds, outlier detection, lower trust in suspicious updates |
The system should fail toward less precision. Showing no nearby result is usually safer than showing a stale or unauthorized dot.
Scaling the system
Most people query near where they are standing. So store and look up location near that area—not in one giant global pile.
One multi-region rule: write location where the user is now; on query, only search map cells inside the viewer’s radius. Don’t spray every region “just in case.” Global friend tracking is a different product (out of scope).
Split data by region or by high-level map-bucket prefix so writes land where users are. Dense cities still need spreading: one concert can overload a single cell’s list of people in that square. Prefer structures that expire stale members by time.
Cache friend sets (they sit on the query path), but keep that cache short and invalidate on block/unfriend/privacy change. A stale friend list is annoying; a stale block is a safety bug—when privacy state is unknown, hide.
Use read replicas or regional caches for graph reads where allowed, but be stricter for blocks and privacy state.
Failure handling
| Scenario | What the user sees | What to build |
|---|---|---|
| Location index lag | Nearby list misses recent movers | Use TTL, version checks, and "last updated" labels |
| Graph service slow | Query times out or returns too much | Cache allowed friends; fail closed on privacy uncertainty |
| Bad GPS accuracy | Dot jumps or appears in wrong place | Accuracy threshold, outlier checks, coarse fallback |
| Privacy update delayed | Paused user still visible | Invalidate privacy cache; hide when state is uncertain |
| Hot cell overload | Slow map at event | Adaptive subcells, candidate caps, 429 for abusive scans |
| Regional outage | Empty or stale results | Degrade to unavailable or stale label; do not fabricate precision |
An outage is not only "API down." In this product, returning an unauthorized exact location is worse than returning an empty list. When privacy state is unknown, hide (fail-closed)—do not guess that someone is visible.
API design
| Endpoint | Role |
|---|---|
POST /v1/location:update | Authenticated client sends location, accuracy, and movement metadata |
GET /v1/nearby/friends | Viewer asks for nearby friends inside a capped radius |
POST /v1/location:pause | User pauses or resumes sharing |
PUT /v1/location/privacy | User updates precision and audience settings |
GET /v1/location/audit | Optional user-facing or support-facing access history |
GET /v1/nearby/friends parameters
| Param | Role |
|---|---|
lat, lng | Viewer location, or omitted if server uses viewer's latest state |
radius_m | Search radius, capped by server policy |
limit | Maximum friends returned |
fresh_within_sec | Optional freshness preference, bounded by product max |
GET /v1/nearby/friends?radius_m=1000
→ Auth viewer
→ Load allowed friends + blocks + privacy
→ Cover radius with S2/geohash cells
→ Fetch cell candidates
→ Intersect with allowed friends
→ Drop stale / hidden / blocked
→ Exact distance for survivors
→ Return coarse buckets + last_seen
Important errors: 403 if the viewer is not allowed to use the feature, 400 if radius exceeds policy, 429 for repeated scans or abusive query patterns, and 200 with an empty list when no allowed fresh friends are nearby.
Observability
Measure both system health and privacy health. A fast query that leaks stale locations is not healthy.
| Signal | Why it matters |
|---|---|
| Location update QPS by client version | Catches battery-draining releases |
| Index lag and stale candidate drop rate | Shows whether moving users leave ghost rows |
| Query p95/p99 by cell density | Finds hot venues and overloaded posting lists |
| Candidate count before and after friend filtering | Confirms spatial pruning and graph filtering are working |
| Block or pause propagation time | Measures safety-critical invalidation |
| Repeated queries per viewer-target pair | Detects stalking-like access patterns |
| Response precision distribution | Verifies coarse display rules are actually applied |
Alert on hot cells, query p99, update storms, and any privacy invalidation delay that exceeds the product bar. For this feature, an audit spike can be as important as an error spike.
Security and abuse
Nearby friends should be opt-in, authenticated, and permission-checked on every path. The server should enforce blocks, pause, hidden mode, and audience tiers; the client UI is not enough. Return coarse distance buckets by default and avoid raw coordinates unless a trusted mode explicitly allows them.
For repeated nearby scans, reuse patterns from the rate limiter guide and rate limiting fundamentals.
Limit query radius and query frequency so someone cannot scan a grid block by block. Detect repeated access patterns against one target, especially when the viewer and target rarely interact. Keep audit trails for sensitive internal tools and for product surfaces where policy requires it.
Retain raw location for as little time as the product needs. If analytics wants movement trends, aggregate or anonymize outside the hot path. Treat exact location, home/work inference, and friend graph intersections as sensitive data. A breach of location history can be more damaging than a leak of many ordinary profile fields.
Cost awareness
Every location update costs mobile battery, radio wakeups, backend writes, and index churn. Reducing update frequency is both a cost and user-trust win. Hot cells cost more than average cities because one posting list can dominate query time.
Returning exact coordinates may look cheap technically, but it creates privacy and compliance cost. Coarse display, TTL, and short retention reduce long-term storage and audit burden. Caching friend sets saves graph QPS, but stale privacy caches can create expensive safety incidents, so invalidation is worth paying for.
Push notifications for "friend nearby" are expensive in user attention. They should be capped and deduped. The default query-on-open model is often cheaper and less creepy.
Production scenes
Location production incidents rarely announce themselves as "geospatial index failed." They show up as battery complaints, stalking reports, ghost dots, and one venue making a whole region slow.
Scene 1: Festival hot cell melts the query path
The moment: During a music festival, nearby query p99 jumps from 200 ms to several seconds. Global QPS is normal. Only one city looks bad.
The trap: The S2 or geohash level is too coarse for the crowd. One cell's posting list contains a huge share of active users. Every viewer opens the map, fetches the same large list, then intersects with friends. The algorithm is correct on average and terrible at the venue.
What to build: Split hot cells into finer subcells, cap candidate scans, cache event-area summaries, and return deterministic top results with a clear "more friends nearby" affordance. Alert on candidate count per cell, not only global QPS.
stadium crowd
→ one cell posting list grows
→ candidate fetch p99 rises
→ every nearby query slows
→ split subcells / cap candidates
Scene 2: Pause sharing does not hide fast enough
The moment: A user pauses sharing after leaving a location. A friend still sees them nearby for several minutes. Support treats it as a privacy incident.
The trap: The app updated privacy state, but the nearby query service used a cached friend or visibility record. The stale cell row still existed and the query did not re-check latest privacy state before returning.
What to build: Invalidate privacy caches on pause and block, store a visibility version, and have the query service prefer "hide" when privacy state cannot be confirmed. Measure pause-to-hidden latency as a safety SLO.
Scene 3: Battery drain after a mobile release
The moment: App-store reviews mention battery drain. Backend location updates double from one client version. Nearby query traffic did not change.
The trap: The client started sending updates on every GPS callback instead of cell change or interval. Backend writes look successful, so availability dashboards stay green while users uninstall the feature.
What to build: Server-side rate limits per device and client version, 429 + Retry-After for noisy clients, kill switch for bad versions, and update QPS dashboards segmented by app build.
Scene 4: Stalking pattern hides inside normal 200s
The moment: A user reports that someone seems to know where they are. API logs show only successful nearby queries, all from a friend account.
The trap: The system authorized each query individually but never looked at the pattern: one viewer querying the same target every minute, across many places, without normal social interaction. Availability and latency metrics were green.
What to build: Rate-limit repeated viewer-target proximity checks, lower precision when risk rises, audit sensitive query patterns, and make blocks take effect immediately. Internal support tools should need reason codes and create audit rows.
In the interview: Pick one production story and tie it back to the design. Hot cells need adaptive indexing. Pause needs privacy invalidation. Battery needs client and server throttles. Stalking needs pattern detection, not just auth.
Bottlenecks and tradeoffs
Precision vs privacy
Finer cells and exact coordinates make the map feel better, but they also reveal routines. Coarse distance buckets and short TTLs reduce risk. In the interview, state the display precision and retention window together because they are one product promise.
Freshness vs battery
Fresh dots require frequent updates. Frequent updates wake GPS and radios, which drains battery and increases backend writes. Update on movement, cell boundary, or interval; show last_seen when freshness is uncertain.
Cell candidates vs friend graph cost
Spatial search reduces the world to nearby candidates. Friend graph filtering reduces candidates to allowed people. The best order depends on friend count and density, but the final design should never degenerate into scanning all users.
Pull vs push
Pull-on-open is calmer and cheaper. Push "friend nearby" is engaging but risky, noisy, and easier to abuse. If push exists, make it opt-in, capped, deduped, and quiet-hours aware.
Interview tips
You have the full design in mind now. These are the common traps interviewers push on.
Raw GPS table is not a nearby system
You might say: "Store each user's latitude and longitude, then query distance."
Interview Push: "Do you scan every user in a city, and do you keep exact coordinates forever?"
Land here: Store short-lived location state and index by cells. Query cells covering the radius, intersect with allowed friends, and compute exact distance only for survivors. Retain raw exact location briefly, return coarse buckets by default, and let TTL remove stale rows.
Friendship does not replace permission
You might say: "If two users are friends, they can see each other nearby."
Interview Push: "What if one paused sharing, blocked the other person, or only shares with close friends?"
Land here: Friend edge is only one input. The query path must enforce opt-in, pause, visibility tier, blocks, and freshness every time. Cached graph data needs invalidation on safety-sensitive changes. If privacy state is unclear, hide the result rather than returning a stale dot.
Every GPS tick is too expensive
You might say: "The phone sends location every second for freshness."
Interview Push: "What happens to battery and update QPS at tens of millions of users?"
Land here: Use significant movement, cell changes, minimum intervals, and app foreground state. The product can feel fresh within minutes without streaming every GPS callback. Server-side rate limits protect the backend from bad client releases.
A stadium cell breaks average-case thinking
You might say: "Cell posting lists are small, so lookup is fast."
Interview Push: "What about a concert where thousands of people fall into the same cell?"
Land here: Watch candidate count per cell. Split hot cells, use finer S2/H3 levels in dense areas, cap candidates, and degrade gracefully. Global QPS can look fine while one venue is broken.
Abuse is not only unauthenticated traffic
You might say: "Only friends can query, so stalking is solved."
Interview Push: "What if an authorized friend queries the same person every minute?"
Land here: Authorization is necessary but not enough. Add rate limits, viewer-target anomaly detection, audit records, precision reduction, and immediate block enforcement. The design must look for patterns, not only single request validity.
Red flags to avoid without correcting yourself: "Store exact GPS forever," "query all users," "friend means always visible," "push everyone nearby by default," "blocks are handled only in the client."
What should stick
After reading this guide, you should be able to explain:
- Map buckets find candidates — divide the map into small areas so you only look nearby; that filter does not decide permission. (Library names like geohash/S2/H3 are optional After one mock.)
- Privacy is on the hot path — opt-in, pause, block, visibility tier, and freshness run before returning results.
- Freshness has a cost — update on movement or interval, not every GPS tick.
- Moving users need versions and TTL — old area rows must expire or be ignored (TTL = hide after N minutes).
- Crowded venues and abuse are production realities — stadiums and repeated target queries need special controls.
Tell it in the room: "Client sends location sparingly after opt-in. Server writes a short-lived local area plus timestamp. Query looks in nearby areas, keeps only allowed friends, drops stale or hidden users, and returns coarse distance plus last_seen—not exact GPS by default."
Related Topics
- System design interview guides - Hub for this walkthrough
- Rate Limiter System Design - Abuse/QPS controls for repeated nearby scans
- Distributed Cache System Design - TTL, hot keys, short-lived location state
- Facebook Feed System Design - Friend graph + privacy filtering contrast
- Notification Service System Design - Why push “friend nearby” needs caps
- Rate limiting and throttling - Fundamentals for query/update throttles
- Redis caching - Friend-set / hot-cell caching patterns
- Authentication and authorization - Opt-in, blocks, and permission on the hot path
- Practice: Nearby Friends - Interactive board + AI feedback
Frequently asked follow-ups
- How often should mobile clients send location updates?
- How do geohash or S2 cells help with nearby search?
- Do you query all users, all friends, or nearby cell candidates first?
- How do pause, block, and opt-out work?
- How do you prevent stalking or exact home-location leaks?
Deep-dive questions and strong answer outlines
Walk through one location update.
The client sends an update only when the user opted in and either moved far enough, crossed a cell boundary, or a minimum interval passed. The server verifies auth and privacy state, snaps or stores the location at the approved precision, updates user->cell and cell->users indexes with a timestamp, and sets a TTL so stale rows disappear.
How does nearby query work?
The query service loads the viewer's allowed friend set and privacy state, computes the cells covering the radius, fetches candidate user ids from those cells, intersects with friends and visibility rules, drops stale rows and blocked users, then computes exact distance for the small survivor set. The response returns coarse distance buckets and last_seen, not exact raw GPS by default.
How do you handle moving users?
Each update removes the user from the old cell and adds them to the new cell, or writes a new version that readers can compare. The record has updated_at and TTL. If cell index and user location briefly disagree, queries prefer recent user location and hide stale entries rather than showing false precision.
How do you protect against stalking?
Require opt-in, mutual visibility, blocks, coarse distance display, rate limits, audit records for sensitive queries, and anomaly detection on repeated queries against one target. Avoid exact coordinates in default responses and expire raw location quickly.
What happens at a stadium?
One cell can contain many users, so posting lists become huge. Use finer subcells or adaptive S2 levels, cap candidate scans, rank deterministically, cache close-friend sets when appropriate, and degrade to "many friends in this area" rather than scanning a city-sized list on every request.
AI feedback on your design
After a practice session, InterviewCrafted summarizes strengths, gaps, and interviewer-style expectations—similar to a written debrief. See a static example report, then practice this problem to get feedback on your own answer.
FAQs
Q: Should the app keep GPS on all the time?
A: No. A practical design updates on movement, cell changes, significant-change APIs, or a minimum interval. Always-on GPS drains battery and creates more privacy risk than most nearby-friends products need.
Q: Is this the same as global map search?
A: No. Nearby friends is friend-filtered and permission-filtered. You are not searching every person in the world. You use cells to find local candidates, then intersect with the social graph and visibility rules.
Q: Why use coarse distance buckets?
A: Exact meters can expose homes, workplaces, and routines. Buckets like "within 500 m" or "near Central Park" often give enough product value while reducing tracking risk. The product may allow more precision for close friends, but it should be an explicit choice.
Q: What does TTL mean for location?
A: TTL is the freshness window after which a location is hidden or marked stale. If a user has not updated for, say, 10 or 15 minutes, the system should not confidently show them nearby. A stale dot can be more harmful than no dot.
Q: How do blocks work?
A: Blocks must be enforced on the query path, not only on the UI. If user A blocks user B, B should not receive A in nearby results even if A's location row still exists in a cell index. Cache friend sets carefully and invalidate on block.
Q: How do you design nearby friends in a system design interview?
A: Opt-in location with a short freshness window; index by geo cells; on query cover the radius with cells, intersect allowed friends and blocks, drop stale rows, refine distance only for survivors; return coarse buckets plus last_seen.
Q: How do you prevent stalking in a nearby friends design?
A: Opt-in and mutual visibility, blocks on the query path, coarse display, radius and QPS limits, viewer–target anomaly detection, short retention, and fail-closed when privacy state is unclear.
Q: What happens at a stadium or concert?
A: One coarse cell can explode. Use finer subcells, cap scans, deterministic ranking, and degrade to "many friends nearby" instead of full posting-list scans.
Practice interactively
Open the practice session to use the canvas and stages, then review AI feedback.