SysDesignPrep.com
Interviewer kit

Design Nearby Friends

Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.

The candidate should have a blank page and a whiteboard, not this.

Open with this

Show each user which friends are within 5 miles, refreshed every 30 s, for 10 M concurrent users, without leaking anyone’s location. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.

The clock

  • 4 min: functional requirements and scope
  • 4 min: non-functional requirements, with numbers
  • 5 min: back-of-envelope estimates
  • 16 min: high-level design and one or two flows
  • 16 min: deep dives and the close

Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.

Requirements · 8 min

Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.

Functional (8)
  • Opt in to share your live location with friends: Off by default. While the app is open it reports a position about every 30 s; with a second opt-in, the OS sends occasional background updates. Sharing is between friends only, never with strangers.
  • See which friends are nearby: Friends within a radius the viewer chooses (5 miles by default), sorted by distance, each with the distance and how fresh it is ("updated 20 s ago").
  • Live refresh while the app is open: Entries move as friends move, about every 30 s. A friend who stops reporting drops off within 10 minutes without anyone deleting anything.
  • Pause sharing: One switch, effective everywhere within a couple of seconds: friends see you disappear at once, not when a timer runs out.
  • Choose who sees you, and block: All friends, or a chosen list. A block hides both people from each other, in the live list and in everything the other could read.
  • Reduced precision: Per friend or for everyone: they see an approximate area (a fixed ~5 km cell) and a rounded distance, never your point. The server guarantees it, not the viewer’s app.
  • Optional location history: Opt-in, kept for later features such as a timeline. The user can view it, choose a retention period and delete all or part of it, and a deletion must reach every copy, backups included.
  • Out of scope: Map rendering, "Alice is nearby" notifications, ETAs and routing, the friend graph itself (an existing service we read), and showing strangers nearby, which we use as the contrast where a geo index is the right tool.
Non-functional (7)
  • Scale (10 M concurrent · ~400 friends each): 1 B users, 100 M use the feature daily, 10 M have it open at the evening peak. Friend lists average ~400 and are capped at 5,000.
  • Freshness (interval 30 s · server path p99 < 1 s): A friend’s move appears within one reporting interval plus under a second through our servers. This is the requirement that drives the hardest trade-off: halving the interval doubles both the battery cost on every phone and the fan-out, which is already the largest stream in the system.
  • Battery and data (≤ 1 radio wake per interval): People uninstall location apps that drain the battery. Uplink and downlink share one radio burst, the interval stretches when the phone is still, and the background uses the OS’s low-power location service.
  • Privacy enforced on the server (block and pause effective < 2 s): Audience, blocks, pause and precision are applied by our servers at publish and at subscribe, never by trusting the viewer’s app. A coarse viewer never receives a precise coordinate, not even inside a payload the app would hide.
  • Availability (99.9 % · degrade to stale): Losing a location update is harmless, because the next one arrives within 30 s; losing a privacy change is not. So location delivery is best effort, and privacy changes are acknowledged and retried.
  • Ephemeral live state, durable history (latest: TTL 10 min · history: opt-in, RF 3): The latest location is a cache that can be lost and rebuilt within one interval. History is the only durable record of where someone has been, so it exists only for users who turned it on.
  • Deletion (reads < 1 min · backups < 30 days): Deleted history disappears from every read path within a minute and from backups within the stated window. Per-user encryption keys make the backup copies unreadable as soon as the key is destroyed.

Estimates · 5 min

Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”

The numbers (8)
  • Location updates in: ~333 k/s peak · ~115 k/s avg. 10 M concurrent ÷ 30 s = 333 k/s at peak, an upper bound because still phones report less often. On average ~3.5 M users have it open (100 M daily × 5 sessions × 10 min ÷ 1 440 min): ~115 k/s. At ~100 B each the ingest is only ~33 MB/s at peak. Writes are not the hard part.
  • Deliveries out of pub/sub: ~13 M/s · ~1.3 M/s reach a phone. Assume ~10 % of a user’s 400 friends are active at the same time at peak (friends share time zones and habits): 40. Each update is delivered to the servers of those 40: 333 k × 40 = 13.3 M deliveries/s. About 1 in 10 of those friends is within 5 miles, so ~1.3 M/s become pushes to a phone. Fan-out is the largest stream in the system, 40 times the updates.
  • Subscriptions held: ~4 B · ~400 GB · 90 % silent. Every active user’s server subscribes to the channels of all ~400 friends, online or not: 10 M × 400 = 4 B subscriptions, at ~100 B of broker bookkeeping each ~400 GB. Only the 10 % to active friends carry traffic; the other 3.6 B are channels nobody publishes on, which cost memory and nothing else.
  • Pub/sub nodes: ~100 primaries (CPU-bound). 13.3 M deliveries/s ÷ ~130 k/s per Redis-class node, kept under ~50 % CPU = ~100 nodes, each with a replica. Memory per node is 400 GB ÷ 100 = 4 GB, nowhere near a limit: deliveries size this tier, not subscriptions.
  • Connection servers: ~400 at 25 k sockets each. 10 M connections ÷ 25 k per server = 400 servers, budgeted low because each socket brings work: 25 k × 40 ÷ 30 s = 33 k deliveries/s in per server, as many distance checks (under a microsecond each), ~3.3 k pushes/s out and ~830 publishes/s. The local subscription map is 25 k × 400 = 10 M entries, ~600 MB.
  • Latest-location cache: ~1 GB · 333 k writes/s · ~7 M key reads/s. One entry per user who reported in the last 10 minutes, ~10 M at peak × ~100 B = ~1 GB. Writes are 333 k/s. Reads come from app opens: ~17 k/s at peak (next row) × 400 friends = ~6.7 M keys/s, sent as one MGET per shard. The cache is sized by operations, not memory: about 20 shards with replicas.
  • Connect churn at peak: ~17 k opens/s · ~7 M subscribes/s. Little’s law: 10 M concurrent ÷ ~600 s per session = ~17 k connects/s. Each subscribes to ~400 channels: ~6.7 M subscribes/s, and as many unsubscribes as sessions end. That is half the delivery load again, which is why a session survives a quick app switch instead of being torn down.
  • History volume: ~10 B points/day · ~500 GB/day. 100 M daily users × 5 sessions × 10 min × 2 updates/min = 10 B points/day, the same as 115 k/s × 86 400. At ~50 B per stored point that is ~500 GB/day, ~180 TB a year before replication, if every user turned history on. The opt-in, downsampling of still periods and retention limits make the real number smaller.

High-level design · 16 min

Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.

Components (14)
  • Mobile app (WebSocket · OS location APIs): Reads the position from the OS and reports it about every 30 s while open, stretching the interval when the phone is still and using the OS’s significant-change service in the background. Holds one WebSocket and renders the nearby list from pushes, computing "updated 20 s ago" from server timestamps. It enforces nothing: it only ever receives what this viewer is allowed to see.
  • Edge load balancer (TLS · least connections): Terminates TLS and places new WebSocket connections by least connections, because sessions last minutes and round robin would leave newly added servers empty. Honours a resume hint so a quick reconnect lands back on the server that still holds the session. Routes the REST settings and history calls to the sharing service.
  • Connection servers (stateful · 25 k sockets each): ~400 servers, each holding one WebSocket per active user plus that session’s state: the viewer’s latest position, their audience, and the last position of each active friend. On an update they apply the sharer’s settings, write the cache, publish once and append history; on each delivery from a friend’s channel they check permission and distance for the local viewer and push only what changed. Broker subscriptions are reference-counted per server, so two local viewers with a common friend cost one.
  • Session registry (Redis · user → server, epoch): Maps a user to (server, connection, epoch), written on connect with an epoch from INCR, so a reconnect always supersedes the session before it. Instead of 10 M per-user TTLs refreshed by heartbeats, each server refreshes one liveness key every 5 s, and an entry that points at a dead server reads as absent. Read only for revokes and duplicate-session kicks, never on the location path.
  • Location cache (Redis · TTL 10 min): The latest location per sharing user: loc:{user} holds the point, the coarse cell and the server timestamp, rewritten with EX 600 on every update, so users who stop reporting fall out without a cleanup job. ~1 GB in ~20 shards with replicas, sized by the ~7 M key reads a second that app opens cause at peak. Losing it costs one reporting interval, because every active user rewrites their entry within 30 s.
  • Pub/sub tier (Redis pub/sub · ~100 primaries): One channel per sharing user, loc:{user}, plus loc:{user}:c carrying only the coarse cell. ~100 independent Redis primaries with replicas, placed on a consistent-hash ring by channel, so an update is one PUBLISH on one node delivered to the ~40 servers subscribed. Fire and forget on purpose: a lost delivery is replaced within 30 s, and anything that must not be lost travels another way.
  • Tier config (etcd · ring vN · interval): The versioned pub/sub ring (primaries and their virtual nodes) and the fleet-wide reporting interval, watched by every connection server. A resize is a new ring version moved through stages (dual subscription, publisher switch, retirement), never an edit in place. Small, rarely changed and needing ordered, confirmed transitions across 400 servers, which is what a consistent store with watches is for.
  • Load governor (control loop · every 10 s): Watches pub/sub CPU, delivery lag and connection-server queues every 10 s and raises the reporting interval in tier config when they cross a threshold, 30 s to 60 s for moving phones. Steps back down slowly with hysteresis so the fleet does not oscillate. The interval is the one lever that cuts updates, deliveries and battery use together.
  • Sharing service (audience · settings · revokes): Decides who may see whom. A viewer’s audience is the friends whose sharing is on and not paused, whose audience includes the viewer, with no block either way, plus the precision each allows, cached per viewer with a version. On any change it writes the store first, then sends acknowledged revokes to the affected sessions found in the registry. Also serves the settings and history-deletion APIs.
  • Sharing store (MySQL · sharded by user): The source of truth for privacy: sharing on or paused, default precision, audience mode, per-friend grants and blocks, each change versioned. A few hundred bytes per user and strongly consistent, because a lost or reordered block is an incident. Read mostly through the sharing service’s cache.
  • Friend graph (existing service · cached): The company’s existing social graph: who is friends with whom, capped at 5,000 friends a person. We read it to build each viewer’s audience and invalidate cached audiences on its friendship-change events. It is the filter of this whole design: we never search by geography, only by this list.
  • Update stream (Kafka · key = user · 3 days): Updates from users with history on, appended by the connection server after the publish, keyed by user id so one user’s points stay ordered on one partition. ~20 MB/s at peak. Three days of retention lets the history writer catch up after an outage and also bounds how long a deleted point can linger here.
  • History writer (batched per user-day): Consumes the update stream and writes points in batches per user and day, minutes behind real time, which nobody notices. Drops points within ~20 m of the previous one while a user is still, encrypts each point under that user’s history key, and skips anything older than a user’s deletion watermark. Idempotent on (user, server timestamp), so a replay after a crash writes nothing twice.
  • Location history (Cassandra · (user, day)): Append-only points partitioned by (user_id, day) and clustered by time: a day of one user is one partition, a range delete is a few partition deletes, and retention is a TTL. ~500 GB a day at the upper bound before replication factor 3. Points are encrypted under per-user keys held in a key service, so destroying a key makes every copy, backups included, unreadable at once.
Flows to ask them to walk (5)
  1. Open the app: first screen, then the stream: The start of a session. Alice must see her nearby friends within a second, although they will not report again for up to 30 s, and an update that lands while her list is being built must not be lost.
    1. The app opens a WebSocket through the load balancer and sends a hello with its first location fix.
    2. The connection server registers the session under a new epoch.
    3. The server asks the sharing service for Alice’s audience: the friends who share with her, and at what precision.
    4. The server subscribes to the channel of every friend in the audience, before reading anything.
    5. The server batch-reads those friends’ latest locations from the cache.
    6. The server computes distances, keeps the friends inside Alice’s radius and sends the first screen.
    7. From now on, deliveries on the subscribed channels keep the list live.
  2. Bob moves: one publish, forty deliveries, four pushes: The core path, 333 k times a second at peak. An update is written once and published once, and the work that depends on the viewer (permission, distance, radius) runs on each viewer’s own server.
    1. Bob’s app sends a location update over its WebSocket.
    2. Bob’s server applies his settings: paused stops here, and if anyone sees him coarsely it also computes his cell.
    3. The server overwrites Bob’s entry in the location cache with a 10-minute TTL.
    4. The server publishes the update once on Bob’s channel, and once more on his coarse channel if he has coarse viewers.
    5. If Bob has history on, the server appends the point to the update stream.
    6. Each subscribed server checks, for its local viewers of Bob, that they may still see him, and computes the distance.
    7. Only viewers for whom Bob is inside the radius, or has just left it, get a push.
    8. Minutes later, the history writer batches Bob’s points into his partition for the day.
  3. New Year’s Eve: concurrency doubles: Around midnight twice the usual peak opens the app at once. Deliveries are updates times active friends, and both double, so fan-out grows four times. The defence is the reporting interval, which the server controls.
    1. Concurrency doubles to ~20 M within the hour, and opens spike as everyone checks who is nearby.
    2. The load governor sees pub/sub CPU and delivery lag cross their thresholds.
    3. The governor raises the reporting interval in tier config.
    4. Connection servers pick up the change and hand each phone the new interval in its next ack.
    5. Servers coalesce pushes harder and drop the subscriptions of viewers who sent the app to the background.
    6. As load falls, the governor steps the interval back down, one step at a time.
  4. A connection server dies with 25 k users: The stateful tier’s worst ordinary day. 25 k phones lose their socket at once, the server’s subscriptions must disappear, and every user must get their list back without a thundering herd on the registry, the sharing service, the cache and the broker.
    1. ws-217 crashes; its broker connections close and its liveness key expires.
    2. Phones notice the dead socket and reconnect with exponential backoff and jitter.
    3. The new server writes the session with the next epoch.
    4. It rebuilds the session: audience, subscriptions, then a snapshot from the cache.
    5. Alice’s friends notice nothing; her next update publishes from the new server.
    6. A revoke that was in flight to ws-217 is re-sent to wherever Alice is now.
  5. Block, pause, and delete my history: The guarantees users actually care about. Each change is written to the store first and then pushed, with acknowledgement, to the servers that enforce it, because unlike a location update, a lost privacy change is an incident.
    1. Bob blocks Alice; the sharing service writes the block and bumps both audience versions.
    2. It finds both live sessions in the registry and sends each server a revoke.
    3. Until the unsubscribe lands, Alice’s server drops anything from Bob by checking her audience version.
    4. Carol pauses sharing; the sharing service records it and tells her server.
    5. Her server stops publishing, deletes her cache entry and publishes a tombstone.
    6. Dave deletes his whole history; the service deletes his partitions and destroys his history key.

Deep dives · 16 min

Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.

Friends are the filter, not geography

Ask: Uber and Yelp put locations in a geo index and query a radius. Why not do that here, or simply have each phone ask for its friends’ locations every 30 seconds?

Good answers name: A channel per user; each active viewer’s server subscribes to all their sharing friends; distance checked at the subscriber, Pull: read every friend’s latest location every 30 s, Geo index of everyone (geohash cells), queried per update and intersected with friends, Publisher pushes directly: look up each friend’s session in the registry and call their server.

Our pick: One channel per sharing user, loc:{user}, and a second, loc:{user}:c, that carries only the fixed cell for coarse viewers. When a viewer connects, their connection server subscribes to the channel of every friend in their audience, online or not, and keeps a map from channel to local viewers with a reference count, so two local viewers with a common friend cost one broker subscription. An update is one PUBLISH; the broker delivers it to the ~40 servers with an active viewer; each server checks permission and distance for its own viewers and pushes only changes, with hysteresis at the radius. Pull survives in one place: the batch read of the cache that builds the first screen. The same shape, Redis pub/sub with a channel per user, is the design in Alex Xu and Sahn Lam’s System Design Interview, Volume 2, so expect an interviewer to know it and to probe the numbers rather than the idea.

  1. Isn’t subscribing to 400 friends who are mostly offline wasteful?
    It costs memory and nothing else. A channel nobody publishes on uses no CPU and no network, and the 3.6 B idle subscriptions are ~360 GB spread over 100 nodes, a few GB each. The alternative, subscribing only to friends who are online, needs a "friend came online" signal sent to every interested server, which is a second fan-out system with its own races. Paying memory to avoid a protocol is a good trade.
  2. Why check the distance on the subscriber’s server rather than before publishing?
    Because the subscriber’s server already holds what the check needs: the viewer’s own position, their radius and their permission for this friend. The publisher would need 40 viewers’ positions per update, 13 M cache reads a second, plus every viewer’s settings. The cost of checking at the subscriber is that the broker carries deliveries for friends who turn out to be far away; at 100 bytes each inside a data centre, that is the cheap side of the trade.
  3. When is a geo index the right answer?
    When the filter is geography, as in a "people nearby" feature for opted-in strangers. The work then mirrors this design: each update is published to the channel of its geohash-5 cell, about 5 km, and each viewer subscribes to the cells covering their radius, re-subscribing as they cross cell boundaries. Hot cells such as a stadium need sampling or a cap on what each viewer receives. The connection tier and the broker are shared; only the channel naming differs.
  4. What if only 1 % of friends were active, not 10 %?
    Deliveries would fall tenfold, to ~1.3 M a second, and the pub/sub tier would shrink to about ten nodes, but the design would not change. Subscriptions and connect churn stay the same, because they depend on how many friends you have, not how many are online. At lower activity the cost shifts toward opening the app, and the grace period that keeps sessions alive across app switches matters more.
  5. A user has 5,000 friends and is at a festival with 300 of them. What breaks?
    Not much, because friend lists are capped (Facebook caps them at 5,000). Opening the app costs 5,000 subscribes and key reads, twelve times normal, and each of their updates goes to ~500 servers, still only 17 deliveries a second. The visible problem is the list: 300 friends within 5 miles means ~10 pushes a second to one phone, so the server sends the nearest 50 with a count of the rest and coalesces to one frame every 10 s. If the product ever let millions follow one person’s location, that would be a broadcast channel needing replicated channels and coarse, delayed updates.
Sharding and resizing the pub/sub tier

Ask: You have 100 pub/sub nodes holding 4 billion live subscriptions. How are channels placed, and what happens when you need 30 more nodes?

Good answers name: Consistent-hash ring over independent Redis primaries, versioned in etcd, resized by a staged dual subscription, Over-provision and resize rarely, Modulo placement (hash(channel) mod N), Redis Cluster with sharded pub/sub, letting the cluster move slots.

Our pick: About 100 Redis primaries, each with a replica, kept at or under 50 % CPU at the normal peak so a doubled night fits after the governor acts. Channels are placed on a consistent-hash ring with ~200 virtual nodes per primary, stored in etcd as ring version N and watched by every connection server. A node failure does not change the ring: the replica is promoted into the same position, servers re-subscribe there and re-read the cache for the friends on that node to cover the seconds of lost deliveries. A resize is ring N+1 rolled out in stages: (1) servers subscribe on the new owner of every moving channel as well as the old one, paced over ~10 minutes, and report ready; (2) once all are ready, publishers switch to the new owner; (3) after two intervals, servers unsubscribe from the old owner. Duplicates during stage 2 are dropped because every delivery carries the update’s server timestamp.

  1. A pub/sub node dies at peak. What do users see?
    For about a second, nothing new from the 1 % of channels it owned. The replica is promoted into the same ring position, so no channel moves. Connection servers see their connection to that node drop, re-subscribe the affected channels on the promoted replica, and do one batch read of the cache for those friends, so anything published during the gap is recovered from the latest entry. At worst a friend’s "updated" age jumps by a few seconds.
  2. Why not publish every update to two nodes so a failover loses nothing?
    It would double the most expensive stream in the system to protect data that is replaced within 30 s anyway. The cache already holds everyone’s latest location, so recovering after a failure is a cheap read, not a replication problem. Spend redundancy on what cannot be recovered: privacy changes and history.
  3. During the switch some publishers use the old node and some the new. Is anything lost?
    No, because in stage 2 every subscriber of a moving channel listens on both. A delivery can arrive twice, once from each node, and the second is dropped because it is not newer than the last one seen for that friend. The rules are only about order: no publisher moves until every subscriber reports ready, and no subscriber leaves the old node until every publisher has moved, plus a margin.
  4. How do you know the tier needs to grow before it hurts?
    Track deliveries per node at peak against the per-node budget, and the trend in active friends per user, because deliveries are updates times active friends and both rise with concurrency. Alert when the forecast peak for the next quarter crosses 70 % of capacity and plan the resize for a quiet morning. Rising delivery lag and broker output buffers are the signs you waited too long.
  5. Why keep the ring in etcd rather than in a config file?
    Because 400 servers must move through stages in a known order and confirm each one, which needs a consistent store with versions, watches and somewhere to record each server’s readiness. A file pushed by a deploy cannot say "everyone is on stage 1 now". The data is tiny and changes a few times a year, which suits etcd exactly.
Connections, reconnects and the first screen

Ask: Your connection servers are stateful. What exactly lives on them, and what happens when a phone reconnects somewhere else?

Good answers name: Stateful WebSocket servers, a session registry with epochs, subscribe-then-snapshot, and a grace period before teardown, Stateless servers that keep viewer state in Redis, Server-Sent Events for pushes plus HTTPS POST for updates, Tear down and rebuild on every disconnect.

Our pick: Phones hold one WebSocket to one of ~400 servers at 25 k connections each. On connect the server takes a new epoch from the registry, fetches the audience, subscribes to every friend’s channel and only then batch-reads the cache and sends the first screen, so nothing that happens during setup is lost. When the socket drops, the session waits for 2 minutes: a reconnect whose resume token routes it back to the same server reattaches and gets a fresh snapshot from memory, with no new subscriptions. After the grace, reference counts are released. A reconnect to another server rebuilds from scratch, and the old server learns from the kick or from the registry epoch that its copy is stale. Deploys drain ~2 % of servers at a time with suggested reconnect delays, which keeps the rebuild burst to a few percent of the normal peak.

  1. What if two sessions for Alice are alive at once?
    The one with the lower epoch loses. The new server kicks the old one when it writes the registry, and the old one checks the registry before acting on a session whenever its own connections recover. Until it closes, a stale session could publish Alice’s location twice, which is harmless because subscribers keep the newest timestamp. What it must never do is keep receiving what her current settings no longer allow, which is why revokes name an epoch.
  2. Why subscribe before reading the cache rather than after?
    Reading first leaves a window: Bob updates after the read but before the subscription exists, and Alice sees his older position until his next update 30 s later. Subscribing first closes it, at the cost of sometimes seeing the same update twice, from the cache and the channel, which the timestamp check sorts out. It is the general snapshot-plus-stream rule: start listening, take the snapshot, apply anything newer.
  3. A region’s load balancer flaps and 3 M phones reconnect within a minute.
    That is ~1.2 B subscribes and as many key reads, far beyond any normal minute. The defences are on both sides: phones back off with jitter and honour a server-suggested delay, servers admit a fixed number of handshakes a second and tell the rest to retry, and the sharing service answers audiences from its cache. Users see "reconnecting" for a minute instead of the broker falling over and turning one flap into ten.
  4. Why not route location updates through the session registry, as WhatsApp routes messages?
    Because a message has one recipient and a location update has forty. Routing by registry means forty lookups and forty RPCs per update, 13 M a second, for data nobody needs reliably. Here the registry is for the rare things that must reach one particular session, revokes and kicks, and channels handle the common case because they let a publisher ignore where its audience is.
  5. How would you know this tier is healthy?
    Connect and reconnect rates per server (a reconnect spike means a crash or a bad deploy), p99 time from hello to first screen, the share of reconnects that reattached within the grace period, subscriptions per server and the age of deliveries on arrival. End to end, test accounts that report fixed positions every 30 s and watch each other measure real freshness per region.
Latest location versus history

Ask: Why keep the current location in a cache with a TTL instead of a database, and why is history a separate store?

Good answers name: Redis with a 10-minute TTL for the latest location; an append-only stream into a wide-column store for history, One database row per user updated in place, with a history table beside it, Write history synchronously in the update path, History kept only on the device.

Our pick: Latest location: loc:{user} holds { lat, lng, cell, ts } in a Redis cluster of ~20 shards with replicas, written with EX 600 on every update; 600 s is twice the longest interval a still phone uses, so stationary friends stay listed and departed ones vanish. History exists only for users who turned it on: the connection server appends to a Kafka topic keyed by user id after publishing, and a writer batches points into Cassandra or Scylla partitioned by (user_id, day) and clustered by time, encrypted under a per-user key, with the user’s retention as a TTL. That is ~500 GB a day at the upper bound. Losing the cache is recovered within 30 s because every active user rewrites their entry; losing history is not recoverable, so it gets replication factor 3 and backups.

  1. The whole location cache is lost. What happens?
    Lists keep working for everyone already connected, because their servers hold friends’ last positions in memory and deliveries keep arriving. App opens see fewer friends for up to 30 s, until every active user’s next update rewrites their entry. Nothing that matters is lost, which is why the cache needs replicas to make failures rare but no persistence.
  2. Why 10 minutes? Why not 60 seconds, two intervals?
    Because the interval is not fixed. A still phone reports every 5 minutes and the governor can stretch everyone under load, so a 60-second TTL would make sitting-still friends flicker in and out. Ten minutes is twice the longest interval in use. The cost is that a friend who closed the app can stay listed for up to 10 minutes, labelled with their real age, unless they paused, which deletes the entry at once.
  3. How do you answer "where was I last Tuesday" from this store?
    One partition read, (user_id, that day), already ordered by time, decrypted with the user’s key. A week is seven partition reads in parallel. That is why the partition is user-day and not user alone: years of one user’s history would otherwise be one enormous partition, and deleting a range would be a scan instead of a few partition deletes.
  4. Should every update go into history?
    No. The writer drops points within ~20 m of the previous one while the user is still, which removes most of a night and most of a day at a desk, and stored precision only needs to be ~10 m. That cuts the upper bound substantially with no loss to any feature built on top. The live stream keeps full resolution because it is gone in 10 minutes.
Battery, bandwidth and throttling

Ask: A location update every 30 seconds for hours on end. How do you keep this from draining the phone, and what do you do when your servers are overloaded?

Good answers name: Adaptive interval by motion and app state, set by the server in every ack, with pushes sent in the same radio burst as the uplink, Fixed 30 s with GPS, always, OS significant-change updates only, Report quickly only while some friend is watching.

Our pick: In the foreground, a moving phone reports every 30 s from network or low-power location, which is accurate enough for distances rounded to a tenth of a mile; GPS is used only if the user opens a map. When activity recognition says the phone is still, it reports only after a move of ~100 m or every 5 minutes to keep its cache entry alive. In the background, sharing continues only with the second opt-in, through the OS significant-change service, one HTTPS POST each. The server sends next_in_s in every ack and holds a viewer’s pushes until just after that viewer’s own update, so the radio wakes once per interval, and the update doubles as the heartbeat. Under load, the load governor raises the interval in tier config, which halves updates and deliveries within one cycle.

  1. How much data does this use?
    About 100 B up and a few hundred down per 30 s, plus WebSocket and TLS framing: well under 100 KB an hour. Data is not the constraint, wake-ups are. That is why coalescing and piggybacking matter more than compression, and why a moving phone sends no heartbeats beyond its updates.
  2. Old app versions ignore next_in_s and keep sending every 30 s during an overload.
    The server enforces the interval anyway: an update that arrives before the session’s current interval has elapsed is acknowledged and dropped, with no cache write and no publish. Those phones waste their own battery but not our capacity. Retiring versions that ignore the field is the long-term fix.
  3. Why not let each phone choose its interval from its battery level?
    It should, within limits: a phone below 20 % can stretch its own interval and say so in its updates. But the server must keep the authority to slow everyone, because broker load is a property of the whole fleet that no single phone can see. The effective interval is the longer of the two.
  4. What does a friend see when someone reports only every 5 minutes?
    The same position with an honest age, "updated 3 min ago", which is correct for someone sitting still. The app shows the age rather than a spinner, so a slow report reads as stillness rather than a fault. When the person starts moving, activity recognition switches them back to 30 s within a fix or two.
Privacy enforced on the server

Ask: Bob blocks Alice, or shares only an approximate location with her. How do you guarantee her phone never receives more than it should?

Good answers name: Audience computed on the server and enforced at subscribe, settings applied at publish, a separate channel for coarse viewers, acknowledged revokes and a check on every delivery, Send everything to the app and let it filter, Random noise added to each published point, End-to-end encrypted locations, with the server relaying ciphertext.

Our pick: The sharing service alone decides who sees whom: a viewer’s audience is the friends whose sharing is on and not paused, whose audience includes the viewer, with no block in either direction, plus the precision each allows. Connection servers subscribe and read the cache only for that audience, and only the coarse channel for coarse friends. Coarse means the fixed ~5 km geohash cell, with distance computed from the cell centre and shown in buckets such as "~3 mi". Every change writes the store first, bumps a version, then sends acknowledged revokes to the live sessions found in the registry, and every delivery is checked against the viewer’s audience version before use. Pause deletes the cache entry and publishes a tombstone. History deletion removes (user, day) partitions, and destroying the user’s history key makes backup copies unreadable at once.

  1. The revoke to Alice’s server is lost. Does Alice keep seeing Bob?
    Not for long, and never in her next session. The sharing service retries until the server acknowledges or the registry shows no live session, and it alerts on revokes unacknowledged after a few seconds. Until then Alice’s server still holds the old audience, which is the one real window, and that is why revokes are acknowledged RPCs rather than pub/sub messages and the target is under 2 s. Any session built after the store write already excludes Bob.
  2. Alice spoofs her own location to trilaterate Bob. How do you stop it?
    In layers. Distances to precise friends are rounded to a tenth of a mile and floored at "< 0.5 mi", and for coarse friends they come from the cell centre, so even perfect trilateration finds only the cell. The server rejects impossible jumps in Alice’s own reports, which makes walking a fake position around a target slow, and it caps how many distinct positions a viewer can report per hour, which defeats automated probing.
  3. Why a fixed cell rather than a blurred point?
    A fixed cell leaks nothing new over time: Bob is somewhere in dr5ru, and a thousand updates say the same thing. A blurred point is a fresh random sample around the truth each time, and the average of many samples converges on the truth. The one leak a fixed cell has is the moment of crossing a boundary, which shows Bob is near that edge; delaying the cell change by a random few minutes blunts it.
  4. A user asks to delete all their history. What actually happens?
    The API sets a deletion watermark, so the history writer skips their points still in the 3-day stream. Partition deletes for every (user, day) remove the data from every read within seconds, and compaction removes the bytes within days. Destroying the user’s history key makes the copies in backups unreadable immediately, and the backups expire within 30 days. The user is told exactly that, because promising instant physical erasure would not be true.
  5. Can employees look up where someone is?
    Not through any ordinary tool. Production access to the cache and to history goes through approved, audited break-glass access, history is encrypted under per-user keys whose use is logged, and analytics see only aggregated, coarsened data. Insider access is a real threat for location data, so it is designed in rather than left to policy.

Close · 5 min

Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.