← Back to practice catalog

System design interview guide

Nearby Friends System Design Interview

Overview diagram for Nearby Friends System Design Interview

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 friendsdrop 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)

  1. Who can share? — Opt-in. Pause/hide must hide them.
  2. Who can see whom? — Friends + blocks + visibility rules on every query.
  3. How does a phone report location? — Only after opt-in, and only when movement or time justifies it (not every GPS tick).
  4. 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.
  5. 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.

Practice Nearby Friends


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)

MinutesFocus
0-5Clarify opt-in, precision, freshness, update policy, and out of scope
5-12Capacity and why updates can dominate reads
12-25HLD: location update path, cell index, friend graph, query path
25-35Data model and detailed read/write design
35-45Architectural dig: moving users, stale cells, and hot events
45-60Privacy, 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.

DimensionRough scaleWhat this means for the design
Active sharersTens of millions globallyMost data should live near where people are; one global pile won’t stay healthy
Location updatesCan reach hundreds of thousands to millions per second if unthrottledClient-side throttling and server admission are required
Friends per userHundreds typical, thousands at the high endFriend intersection must be bounded and cached carefully
Hot cellsStadiums, airports, campuses, festivalsThe list of user ids in one cell (a posting list) can become huge; need caps or finer subcells
Freshness1-15 minutes depending on productTTL 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/location and cell_id -> user ids with 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

OrderWhen it worksCost risk
Friends firstTiny friend graphsEvery map open walks all friends’ locations
Cells first, then friendsLarge graphs, high QPS, dense venuesSpatial 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.

EntityKey fieldsWhy it exists
location_stateuser_id, cell_id, coarse lat/lng, accuracy, updated_at, version, TTLCurrent fresh location for a sharing user
cell_membershipcell_id, user ids, update version or scoreCandidate lookup for nearby query
privacy_stateuser id, sharing mode, precision tier, paused flag, allowed audiencesDecides whether location may be returned
friend_edgeuser A, user B, state, close-friend tierFriend filtering and optional precision tier
block_edgeblocker, blocked, updated_atMust remove visibility immediately
query_auditviewer, target or candidate count, time, reason, toolAbuse 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:

  1. 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.
  2. Server checks auth and sharing state (POST /location:update). Paused/opted-out → reject or store only allowed coarse state.
  3. Map the point into a cell id (bucket on the earth—geohash, S2, or H3).
  4. Write latest state (“user U is in cell C at time T”) and cell membership (“cell C contains U”), with a freshness expiry (TTL).
  5. If they left old cell O, remove or expire membership in O. A version number helps ignore late old writes.
  6. 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)

  1. Start with the viewer: may they use the feature?
  2. Load allowed friends and blocks (GET /nearby/friends).
  3. 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.
  4. Fetch candidate user ids from those cells; merge and dedupe.
  5. 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.
  6. 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.

Nearby friends search must filter cell candidates by friendship, block state, sharing opt-in, freshness, and distance bucket before returning approximate results

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

ChallengeWhat users see if it goes wrongWhat to say in the interview
Battery drainPhone gets hot; users disable the featureUpdate on movement, cell change, or interval; use OS significant-change APIs
Stale dotsFriend shown nearby after leavingTTL, last_seen, stale hiding, and version checks
Hot cellsMap query slow at concertsAdaptive subcells, caps, deterministic ranking, and graceful coarse results
Privacy leaksExact home or commute exposedCoarse buckets, opt-in, pause, short retention, audit
Block churnBlocked user still sees locationEnforce blocks on query path and invalidate graph cache
Mock GPS or impossible jumpsUser teleports across continentsAccuracy 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

ScenarioWhat the user seesWhat to build
Location index lagNearby list misses recent moversUse TTL, version checks, and "last updated" labels
Graph service slowQuery times out or returns too muchCache allowed friends; fail closed on privacy uncertainty
Bad GPS accuracyDot jumps or appears in wrong placeAccuracy threshold, outlier checks, coarse fallback
Privacy update delayedPaused user still visibleInvalidate privacy cache; hide when state is uncertain
Hot cell overloadSlow map at eventAdaptive subcells, candidate caps, 429 for abusive scans
Regional outageEmpty or stale resultsDegrade 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

EndpointRole
POST /v1/location:updateAuthenticated client sends location, accuracy, and movement metadata
GET /v1/nearby/friendsViewer asks for nearby friends inside a capped radius
POST /v1/location:pauseUser pauses or resumes sharing
PUT /v1/location/privacyUser updates precision and audience settings
GET /v1/location/auditOptional user-facing or support-facing access history

GET /v1/nearby/friends parameters

ParamRole
lat, lngViewer location, or omitted if server uses viewer's latest state
radius_mSearch radius, capped by server policy
limitMaximum friends returned
fresh_within_secOptional 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.

SignalWhy it matters
Location update QPS by client versionCatches battery-draining releases
Index lag and stale candidate drop rateShows whether moving users leave ghost rows
Query p95/p99 by cell densityFinds hot venues and overloaded posting lists
Candidate count before and after friend filteringConfirms spatial pruning and graph filtering are working
Block or pause propagation timeMeasures safety-critical invalidation
Repeated queries per viewer-target pairDetects stalking-like access patterns
Response precision distributionVerifies 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:

  1. 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.)
  2. Privacy is on the hot path — opt-in, pause, block, visibility tier, and freshness run before returning results.
  3. Freshness has a cost — update on movement or interval, not every GPS tick.
  4. Moving users need versions and TTL — old area rows must expire or be ignored (TTL = hide after N minutes).
  5. 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."

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.