Skip to the solution
HardSystem Design14 min read

How would you design Nearby Friends?

0 views

The problem

Question presented to candidate: "We want a 'Nearby Friends' feature: if you opt in, you see which of your friends are within about 5 km, with the distance, and the list updates live while the app is open. Walk me through how location updates get from one phone to the right friends, and how you keep it fast and private at a hundred million daily users."

What a strong answer should cover:

  • The difference from a driver search: the audience is your friends, not everyone nearby, so the core is fan-out over the social graph, not a geo index
  • Clients hold a WebSocket; each update is written to a location cache with a TTL and published to the user's pub/sub channel; WebSocket servers subscribe to the channels of each connected user's friends
  • Distance filtering at the receiving side (most deliveries are dropped: 3.9% passed in the example), and why that is cheaper than indexing
  • Capacity: about 333,000 updates/s and 13 million pub/sub deliveries/s under the stated assumptions; sharding pub/sub channels across many nodes
  • Privacy and lifecycle: explicit opt-in, both sides must share, coarse distance, location history kept separately or not at all, TTL so a closed app disappears
  • The optional "nearby strangers" extension, which does need a geo index

Clarifying questions expected:

  • How many daily users, how many friends on average, and how often does a moving phone report?
  • Do we show exact positions or only "1.2 km away"? Do we keep location history?
  • Must both friends opt in, and should the list be live or refreshed on open?

Code / implementation expected: Optional β€” the design with numbers; the example simulates per-user channels, fan-out and the receiver-side filter.

  • system-design
  • case-study

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

14 min read Β· 16 sections

Read the code

Nearby Friends fan-out: per-user pub/sub channels, receiver-side distance filter, and a TTL location cache
// Nearby Friends: per-user pub/sub channels, receiver-side distance filter, TTL location cache.
let seed = 7;
function rand() { seed = (seed * 48271) % 2147483647; return seed / 2147483647; }
function randInt(n) { return Math.floor(rand() * n); }

const USERS = 2000, FRIENDS_EACH = 20, RADIUS_KM = 5, TTL_S = 600, SERVERS = 4;
const friends = Array.from({ length: USERS }, () => new Set());
for (let u = 0; u < USERS; u++) {
  while (friends[u].size < FRIENDS_EACH) { const v = randInt(USERS); if (v !== u) { friends[u].add(v); friends[v].add(u); } }
}
const online = [], pos = [];
for (let u = 0; u < USERS; u++) { online.push(rand() < 0.3); pos.push([rand() * 40, rand() * 40]); } // a 40 km x 40 km city
let edges = 0; for (const f of friends) edges += f.size;
console.log("users " + USERS + ", avg friends " + (edges / USERS).toFixed(1) + ", online " + online.filter(Boolean).length);

// Location cache: user -> { x, y, expiresAt }, standing for Redis keys with a TTL.
const cache = new Map();
// Pub/sub: channel = user id; subscribers = the WebSocket server of each online friend.
const subs = new Map(); let subscribeCalls = 0;
function connect(u) {
  for (const f of friends[u]) { if (!subs.has(f)) subs.set(f, new Set()); subs.get(f).add(u); subscribeCalls++; }
}
for (let u = 0; u < USERS; u++) if (online[u]) connect(u);
console.log("channel subscriptions opened by the online users: " + subscribeCalls);

function dist(a, b) { return Math.hypot(pos[a][0] - pos[b][0], pos[a][1] - pos[b][1]); }
const perServer = new Array(SERVERS).fill(0);
function publishRound(now) {
  let published = 0, delivered = 0, pushed = 0;
  for (let u = 0; u < USERS; u++) {
    if (!online[u]) continue;
    cache.set(u, { x: pos[u][0], y: pos[u][1], expiresAt: now + TTL_S });
    published++;
    for (const s of subs.get(u) || []) {
      if (!online[s]) continue; // the subscriber has gone; real pub/sub would have dropped it on disconnect
      delivered++; perServer[s % SERVERS]++;
      if (dist(u, s) <= RADIUS_KM) pushed++; // the receiving server filters by distance before pushing to the phone
    }
  }
  return [published, delivered, pushed];
}
const [p, d, s] = publishRound(0);
console.log("one 30 s round: published " + p + ", pub/sub deliveries " + d + ", pushed to phones " + s + " (" + (100 * s / d).toFixed(1) + "% pass the " + RADIUS_KM + " km filter)");
console.log("deliveries per WebSocket server: " + perServer.join(" "));

function nearbyList(u, now) {
  const out = [];
  for (const f of friends[u]) {
    const c = cache.get(f);
    if (!c || c.expiresAt <= now) continue;
    const km = Math.hypot(c.x - pos[u][0], c.y - pos[u][1]);
    if (km <= RADIUS_KM) out.push([km, f]);
  }
  out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  return out.map(([km, f]) => "user " + f + " " + km.toFixed(1) + " km");
}
let me = 0; while (!online[me] || nearbyList(me, 0).length < 2) me++;
console.log("user " + me + " opens the app, initial list from the cache: " + nearbyList(me, 0).join(", "));

const friend = [...friends[me]].filter((f) => online[f]).sort((a, b) => dist(me, a) - dist(me, b))[0];
console.log("friend " + friend + " is " + dist(me, friend).toFixed(1) + " km away and walks away 1 km per round:");
for (let r = 1; r <= 4; r++) {
  pos[friend][0] += (pos[friend][0] >= pos[me][0] ? 1 : -1);
  cache.set(friend, { x: pos[friend][0], y: pos[friend][1], expiresAt: r * 30 + TTL_S });
  const km = dist(me, friend);
  console.log("  round " + r + ": " + km.toFixed(1) + " km -> " + (km <= RADIUS_KM ? "pushed, shown" : "dropped by filter"));
}

online[friend] = false; // the friend closes the app: no more publishes, cached location ages out
const last = 4 * 30;
for (const age of [300, 599, 600]) {
  const c = cache.get(friend);
  console.log(age + " s after the last update: " + (c.expiresAt > last + age ? "location still cached" : "location expired, friend leaves every list"));
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 103 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextHard

How would you design Google Drive?

Continue