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 probe | Answer 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
Designing Ad Click Aggregation: Exactly-Once Counting at Scale
Billions of clicks, billed to the cent: streaming aggregation with watermarks, dedupe, idempotent sinks, and lambda-style reconciliation.
Designing a CDN: Cache Hierarchy, Invalidation, and Request Routing
How a CDN actually works: edge PoPs, origin shields, consistent-hash cache keys, purge fan-out, and the anycast vs DNS routing decision.
Designing a Distributed Cache Service: Redis Cluster Internals and Hot Keys
Build the cache, not just use it: slot-based sharding, gossip and failover, eviction under memory pressure, and the hot-key problem that shards can't solve.
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.