System design interview guide
Nearby Friends System Design
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.
The interview is partly about geospatial indexing, but only partly. The real design is a privacy-sensitive, battery-limited, friend-filtered location system. You are not building a global "find all users near me" search. You are answering a narrower question: 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 locally, filter by friendship and privacy, then return coarse results. A geohash or S2 cell is only a candidate filter. The permission rules and freshness window decide whether a candidate may actually appear.
Weak answers run Haversine distance against every friend or store exact GPS forever "for analytics." Strong answers pick a cell index, cap freshness, enforce opt-in and block on every path, and explain how the product behaves when the location is stale or uncertain.
How to approach
Start with product boundaries before data structures. Ask what precision the product needs, who can see whom, how often phones may update, and what to do with stale locations. Then walk one update and one nearby query.
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."
Now you can snap or fuzz locations and avoid returning raw coordinates in the default response.
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."
That gives you a TTL and prevents old locations from pretending 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."
Now you can update on cell change, significant movement, or minimum interval instead of sending 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."
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 |
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 | Partition by region or cell prefix |
| 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 | Posting lists can become huge; need caps or adaptive subcells |
| Freshness | 1-15 minutes depending on product | TTL and last_seen are part of correctness |
Three lessons follow. First, do not send every GPS tick. Second, do not scan every user in a city. Third, never let stale or hidden locations look precise. The system should prefer an empty or "last seen 12 minutes ago" result over a confident lie.
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, snaps or quantizes the location, 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 Haversine over all users the first step.
Core design approaches
Geohash, S2, H3, or similar cells
All of these map latitude and longitude into cell ids. A cell id lets the system ask "who is in these nearby cells?" without scanning the world. Geohash is easy to explain because nearby prefixes roughly group places, but it has edge cases near cell boundaries. S2 and H3 use hierarchical cells that are often easier to tune for different resolutions. In an interview, naming one and explaining the candidate-filter pattern matters more than arguing brand names.
User-to-cell vs cell-to-users
user_id -> cell is cheap for writing the latest location and answering "where is this user now?" It is not enough for nearby search because the query would still have to inspect many friends one by one. cell_id -> user_ids is better for nearby queries: compute cells in the radius, fetch users in those posting lists, and then filter. Most real designs keep both, because writes need latest-state lookup and reads need reverse lookup.
Query cells first vs graph first
If a viewer has 300 friends, fetching all friends and checking distances may be acceptable for small products. At large scale and high QPS, the safer pattern is to use spatial cells to shrink candidates, then intersect with the friend set. If the friend set is tiny, either order can work. 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
The design has two journeys: a phone reports location, and a viewer asks who is nearby. Keep both journeys tied to privacy state. A location update from a paused user should not enter the index, and a nearby query should not return a user just because their cell row still exists.
When the user opts in, the client does not start streaming GPS every second. It uses a battery-aware policy: send when the user crosses a meaningful cell boundary, moves more than a threshold distance, opens the map, or a minimum interval passes. The update includes location, accuracy, timestamp, and sometimes a signal about whether the OS thinks the location is mocked or low quality. If accuracy is poor, the client can delay or send a coarser cell rather than pretending precision it does not have.
The server receives POST /location:update, authenticates the caller, and checks the user's sharing state. If the user paused sharing, opted out, or is in a mode that should hide exact location, the server either rejects the update or stores only the allowed coarse state. Then it maps the location to a cell id at the selected resolution. A cell is a bucket on the earth; S2, H3, and geohash are common ways to create those buckets.
Writing location has to update two views. The latest-state row says "user U is currently in cell C with updated_at T." The reverse index says "cell C contains user U." If the user moved from old cell O to new cell C, the system removes or expires membership in O and adds membership in C. A version number helps readers ignore older writes that arrive late. If the two indexes briefly disagree, query code should check the user's latest-state row and timestamp before returning them.
For GET /nearby/friends, the query service starts with the viewer, not the world. It checks that the viewer may use nearby friends, loads the allowed friend ids and block rules, and gets the viewer's own current or supplied location. It computes all cells covering the requested radius, including neighboring cells so friends near a border are not missed. It fetches candidate user ids from those cells, merges and dedupes them, then intersects with the allowed friend set. Only after that does it compute exact distance for survivors.
Freshness and privacy are final filters, not UI decoration. Drop any candidate whose location is older than the freshness window. Drop anyone who paused, blocked the viewer, is not mutually visible, or is visible only at a coarser tier than the query requested. The response should include fields like approx_distance_bucket, last_seen, and maybe a coarse map center. It should not return exact raw GPS by default.
Moving users make the design interesting. A person walking through a festival can cross cells often. If every crossing writes immediately, the backend gets hot and the battery drains. If updates are too slow, the map lies. The practical compromise is minimum intervals, movement thresholds, and cell-resolution choices that match the product. You can also coalesce updates: if three movements happen quickly, keep the latest and avoid writing every intermediate point.
In the room (spoken recap): "The client sends location only when opted in and movement or time justifies it. The server checks privacy, snaps to a cell, writes latest state plus cell membership with TTL, and uses versions to ignore 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
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
Partition location data by geographic region or high-level cell prefix so most writes land near where users are. Very active cities still need shard spreading because a concert, airport, or campus can overload a single cell. For cell posting lists, choose data structures that support TTL or score-by-updated-time so stale members are cheap to remove.
Cache friend sets because graph lookups sit on the query path. Keep the cache short-lived and invalidate on block, unfriend, or privacy changes. If a user has thousands of friends, precompute close-friend subsets or cap intersection work so one high-degree account does not dominate p99.
Use read replicas or regional caches for graph reads where allowed, but be stricter for blocks and privacy state. A stale friend list is a quality issue; a stale block can be a safety issue. When unsure, hide the location.
For multi-region travel, write location to the region that owns the current cell and route queries based on viewer location. If friends are spread globally, query only cells in the viewer's radius; do not fan out across every region unless the product is asking for global friend location, which is out of scope here.
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. Treat privacy uncertainty as fail-closed.
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.
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:
- Cells are candidate filters — geohash, S2, or H3 narrows the search area; it does not decide permission.
- 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 cell rows must expire or be ignored.
- Hot cells 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 snaps to a cell, writes latest location and cell membership with TTL. Query covers radius with cells, fetches candidates, intersects friends and blocks, drops stale or hidden users, computes distance for survivors, and returns coarse buckets plus last_seen."
Key Takeaways
- Clarify precision first — exact pin vs coarse bucket changes privacy, storage, and API response.
- Use cells for pruning — query neighboring cells, not every user or every friend in the world.
- Intersect with the graph — friends, blocks, visibility, and opt-in decide final eligibility.
- TTL stale locations — old dots are worse than no dots when safety is involved.
- Throttle updates — battery and backend writes matter as much as query latency.
- Protect against stalking — repeated authorized queries can still be abusive.
- Operate dense places — hot cells need candidate caps and adaptive resolution.
Related Topics
- Location Based Services System Design - Broader geo indexing and location product patterns
- Facebook Feed System Design - Friend graph fan-out and privacy filtering contrasts
- Distributed Cache System Design - Short-lived state, TTL, and hot key behavior
- Rate Limiter System Design - Abuse controls for repeated nearby scans
- Notification Service System Design - Optional push alerts and why they need caps
Quick revision notes
Use this as a last look before a mock interview, not as a replacement for the full guide.
Clarify first. Ask about opt-in, mutual visibility, precision, freshness, update frequency, and out-of-scope global search. If you skip this, you may design a creepy tracking system instead of a nearby friends feature.
Draw the update path. The client sends location only when opted in and movement or time justifies it. The server checks privacy, maps the point to a cell, updates latest state and cell membership, and applies TTL.
Draw the query path. Viewer auth and privacy come first. Then compute covering cells, fetch candidates, intersect allowed friends and blocks, drop stale rows, compute exact distance for survivors, and return coarse results.
Remember moving users. A user can leave one cell and enter another while writes arrive late. Use versions, updated_at, TTL, and latest-state verification so old membership does not create ghost dots.
Operate safety. Watch update storms, hot cell candidate counts, stale drop rate, pause-to-hidden latency, and repeated viewer-target queries. Privacy failures are production incidents even when APIs return 200.
Practice this path: Nearby Friends System Design.
What interviewers expect
A strong answer does not scan all users or all friends for every map open. It stores short-lived location state, maps each location into cells, queries the cells covering the viewer's radius, intersects candidates with the allowed friend graph, filters stale and blocked users, then computes exact distance only for survivors. It also names privacy rules, battery limits, freshness windows, and abuse detection as first-class design constraints.
Interview workflow (template)
- Clarify requirements. Confirm functional scope, users, consistency needs, and which non-functional goals matter most (latency, availability, cost).
- Rough capacity. Estimate QPS, storage, and bandwidth so your data model and partitioning story are grounded.
- APIs and core flows. Define a minimal API and walk 1–2 critical read/write paths end to end.
- Data model and storage. Choose stores for each access pattern; call out hot keys, indexes, and retention.
- Scale and failure. Add caching, sharding, replication, queues, or fan-out as needed; say what breaks in failure modes.
- Tradeoffs. Name alternatives you rejected and why (e.g. strong vs eventual consistency, sync vs async).
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.
Practice interactively
Open the practice session to use the canvas and stages, then review AI feedback.