Section 4: Three Doors and a Dial - API Design (with Pushback)
Executive Summary: This section turns the contract of Sections 2 and 3 into an interface: exactly three surfaces — a batched location write that returns the next recommended cadence, a WebSocket subscription that delivers arrival and departure events, and a bootstrap read for app open — and it defends each one under pressure. Two points separate a Staff answer from a competent one here: the server, not the phone, owns the update cadence, which turns a battery setting into a fleet-wide operational lever; and the client never supplies its own friend list, because an API that accepts one is an API for reading strangers' locations.
Section 3 left you holding a write rate of about 333 thousand location updates a second and a post-filter fan-out of roughly 1.3 million messages a second. The interviewer's next question is the practical one: "So what does the phone actually send, and how often?" This is where many candidates reach for a REST resource sketch and start naming fields. Resist that. The interface for a moving-presence system is not a CRUD surface over a location table — it is a small set of channels, each matched to a different temporal shape of the problem.
Three Surfaces, and Why There Are Only Three
There are exactly three things anyone needs to do. Maya's phone needs to tell the system where she is. Maya's phone needs to hear about friends as things change. And when Maya opens the app cold, it needs to know the answer immediately without waiting for someone else to move.
POST /v1/location
Authorization: Bearer <session token>
{ "points": [ { "lat": 37.7601, "lon": -122.4269, "accuracy_m": 12,
"device_ts": "2026-10-26T14:14:03Z" } ],
"motion": "walking" }
-> 202 Accepted
{ "received_at": "2026-10-26T14:14:03.412Z", "next_interval_s": 30 }
WS /v1/subscribe (upgrade; identity from the session token)
<- { "type": "friend_nearby", "friend_id": "u_raj", "distance_km": 1.2,
"as_of": "2026-10-26T14:14:04Z" }
<- { "type": "friend_moved", "friend_id": "u_raj", "distance_km": 0.8 }
<- { "type": "friend_left", "friend_id": "u_nina", "reason": "departed" }
GET /v1/nearby
-> 200 { "friends": [ { "friend_id": "u_raj", "distance_km": 1.2,
"as_of": "2026-10-26T14:14:04Z" } ],
"stale": [ { "friend_id": "u_sam", "last_seen_min": 12 } ] }
The write is batched and the receive time is server-assigned. Batching exists because a phone that lost signal for four minutes has eight buffered points and should spend one radio wake-up, not eight; the server-assigned timestamp exists because a device clock is attacker-controlled and drifting, and Section 8 builds its ordering rule on exactly this. The response returns 202: the hot path acknowledges within the 100 millisecond p99 the contract promises and lets the fan-out happen behind the ack, because a lost update is superseded 30 seconds later and never retried.
The subscription is the live path, and note what the client does not do: it never names a channel. It opens a socket, and the session server subscribes it — once, at session establishment — to the one channel that belongs to this user. Every online user has exactly one such channel, so the broker holds about 10 million subscriptions, one per online session, and a reconnect costs exactly one resubscribe. The direction matters: when Maya moves, the location service publishes the resulting events into the channels of the friends who should hear about it, not into a channel of Maya's that others watch. Three event types ride that channel, and the middle one earns its keep. friend_nearby and friend_left mark the two edges of a pair's state machine; friend_moved is what keeps the distance label live between those edges, so the row that said "Raj, 1.2 km" updates to "0.8 km" as he walks toward you instead of freezing until he leaves the band. It is also where the distinction between an event and an alert gets drawn: the stream carries every genuine state change — including the friend_nearby transition itself — so the nearby list stays correct, while the per-pair one-hour cooldown of Section 8 gates only the user-visible alert and the offline push. A pair under cooldown still emits events; it just stops buzzing the phone.
The bootstrap read exists because a push-only API has a cold-start hole — open the app while the whole city stands still and you see nothing until somebody walks. One round trip at a 300 millisecond p99 closes it, and it doubles as the repair path whenever a client suspects it missed messages: a broker shard loss in Section 11 is recovered by calling it, not by replaying a log.
Notice the two surfaces that are absent. There is no GET /friends/{id}/location, because the product question is never "where is Raj" — it is "who is near me", and a per-friend lookup endpoint is a stalking primitive with a REST path. And there is no history endpoint; Section 3 priced that stream at about 2.9 terabytes a day of the most sensitive data the company could hold and declined it, so there is nothing to serve. The flip trigger is explicit: an opt-in, time-boxed "share my route" product (Section 12) earns its own encrypted store and its own endpoints — never a read path bolted onto this cache.
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson