5 min readRishi

Designing Nearby Friends: Real-Time Location Sharing at Scale

"Nearby friends" looks like Uber's driver map at first glance — live dots moving on a phone screen — but the query is inverted, and the inversion changes the whole architecture. Uber asks "who is near this point?" — a spatial query over strangers. Nearby friends asks "which of my friends are near me?" — a query over a small fixed set, repeated for millions of overlapping sets. Get that distinction in the first two minutes of the interview and the rest of the design follows almost mechanically.

Requirements

Functional: opted-in users see friends within X km with distance, updated within ~30 seconds; users toggle sharing off instantly. Non-functional: 100M active sharers, location update every 15-30s (~5M updates/sec peak), staleness under 30s, and location data is privacy-critical — bounded retention, audited access, hard delete.

Why you don't need a geospatial index

A user has a few hundred friends, of whom a handful share location. The "nearby" computation per update is: fetch friends' last positions, compute distances, filter by radius. That is a set lookup + arithmetic over ~50 points — no geohash, no H3, no spatial tree. A spatial index answers "who is near point P among everyone?"; we never ask that question. (The moment product adds "people near you," then you bolt on the geo-sharded index — say this trade explicitly.)

So the core state is one record per sharing user, in memory with TTL:

redis: loc:{user_id} -> (lat, lon, updated_at)   TTL 10 min

The TTL is doing quiet privacy work: stop sending updates (app closed, sharing off) and your dot evaporates by construction — absence of writes becomes absence of data, with no cleanup job to forget.

The fan-out: pub/sub per user

The push path is where the design earns its complexity. When user U moves, every online friend viewing the map should see it within seconds. Polling 50 friends' positions every 30s per viewer is O(viewers × friends) reads of mostly-unchanged data; push inverts it to O(updates × online-friends).

U's phone --update--> WS gateway --> location service
    1. write loc:{U} (TTL)
    2. publish to channel loc_updates:{U}
friends' map sessions subscribe to loc_updates:{F} for each sharing friend F
    -> receive U's position -> compute distance client/edge-side -> render

Each viewer's WebSocket session subscribes to the channels of their sharing friends (Redis pub/sub or a message broker; channels are cheap). The fan-out multiplier is bounded by friend-graph degree — a few hundred — not by celebrity followings, which is exactly why the news-feed "celebrity problem" does not appear here. The friendship graph caps your blast radius by design; when an interviewer asks "what about a user with 5M followers," the answer is that this product is friends-only and mutual — and if product changes that, you switch that user to pull.

Presence layering: only online friends need pushes. Track viewer sessions (presence set per user); publish-side or subscribe-side filtering keeps offline users out of the fan-out entirely. Offline friends catch up with one batch read when they open the map — push for the live, pull for the returning.

Scaling the moving parts

WebSocket tier: ~10-20M concurrent connections at peak → hundreds of gateway nodes; users hash to gateways; gateways subscribe to the union of channels their connected users need. Gateway death → clients reconnect (jittered) and resubscribe — all state is reconstructible from subscriptions + Redis, so the tier is effectively stateless.

Pub/sub tier: 5M msgs/sec × ~10 online friends = ~50M deliveries/sec at peak — sharded by channel (user_id) across a broker cluster. This is the load number to say out loud; it justifies every "bounded fan-out" decision above.

Update cadence adaptivity: phones battery-throttle GPS; stationary users send every few minutes (or the server suppresses no-op updates — moved under 50m → skip publish). Cuts write and fan-out volume by most of half.

Privacy as an architectural constraint

This is the part senior interviewers wait for, because it changes storage decisions rather than being a compliance paragraph:

  • Ephemerality by default: live positions in TTL'd memory only. If history is a feature (location timeline), it is a separate, encrypted, per-user store with explicit retention — never a side effect of the live path. No raw locations in general-purpose logs.
  • Authorization at publish and subscribe: subscription to loc_updates:{U} is checked against U's sharing list at subscribe time, and revocation (unfriend, toggle off) actively kills subscriptions — a revoked friend must stop receiving updates in seconds, not at next session. An access change is an event propagated through the same real-time machinery, not a row update waiting to be re-read.
  • Fuzzing options: "within 1 km" tiers computed server-side, so precise coordinates never reach users granted only coarse visibility. Precision is enforced where data leaves the trust boundary, not in the UI.
Interview probeAnswer sketch
Why not query a geo-index per viewer?The candidate set is the friend list (~50), not the population; set lookup beats spatial query
Friend appears after toggle-off?TTL expiry + active unsubscribe + tombstone push ("U stopped sharing") for instant UI removal
30s staleness OK?Product says yes — and it buys 10x less load than 3s; state the freshness-cost curve
Cross-region?Users pinned to home region; cross-region friend pairs relay via inter-region pub/sub bridge (rare, bounded)

The reusable insight: fan-out cost is governed by the graph you fan out over. Friends-graphs are bounded and mutual — push works. Follower-graphs are unbounded and asymmetric — push breaks, hybrids appear. Same dots on a map, different graph, different system.

Keep reading

Newsletter

New posts, straight to your inbox

One email per post. No spam, no tracking pixels, unsubscribe anytime.

Comments

  • No comments yet. Be the first.