Design friend suggestions
Design "people you may know" for the social app from D39: a short list of accounts each user is likely to want to follow, shown on the home screen and after sign-up. The graph has 400 billion edges, so the obvious approach (look at friends of friends) produces trillions of candidates a day. The design is about cutting that down without losing the good ones, keeping suggestions fresh, and never suggesting someone in a way that leaks private information.
1. The prompt
"Design the 'people you may know' feature: suggest accounts a user might want to follow."
What makes it hard:
- The candidate space explodes. Friends of friends for one user can be tens of thousands of people; for a user who follows a celebrity, hundreds of millions.
- Freshness matters most right after an action. Someone who just followed five colleagues expects to see the rest of the team, not yesterday's list.
- A suggestion can reveal a secret. "Why is my therapist suggesting her other patients?" A suggestion must never be explainable only by private data.
2. Frame the problem
Where are suggestions shown?
A carousel on the home screen, a full "suggested for you" page, and right after sign-up.
Each placement shows 10–30 people; the system must produce a ranked list, not a set.
What counts as success?
The user follows the suggestion, and the follow lasts (they don't unfollow or block within weeks). For two-way networks, the other side accepts.
Optimising clicks alone rewards suggesting celebrities or strangers who'll be dismissed later.
How fresh?
A daily full refresh is fine for most users; after a follow or a contact upload, new suggestions within a minute.
That suggests a batch pipeline plus a near-real-time path for recent actions.
Which signals may we use?
The follow graph, interactions (likes, replies, profile views), uploaded contacts with consent, declared school or workplace. Not private messages, not precise location history.
Each signal has a privacy cost; the product team decides, the design must enforce it.
Scale?
2 billion accounts, 400 million daily actives, 2,000 suggestion requests a second at peak (lists are cached in the app).
Serving is small; the offline computation is the heavy part.
3. What it must do
- Return a ranked list of suggested accounts for a user, quickly, for each placement.
- Find candidates from several sources: friends of friends, contacts, shared school or workplace, people who interact with the user.
- React to new actions: a follow, a dismissal or a contact upload changes the next list.
- Respect limits: never suggest blocked users, people already followed, dismissed suggestions, or anyone who opted out of being found by phone number or email.
Not now: suggested groups or pages, explaining the model's internals, ads ("promoted accounts" go through the ad system, D41).
4. How well it must do it
| Target | Why this number |
|---|---|
| Serving under 100 ms at p99 speed | The carousel loads with the home screen. |
| Fresh after an action within 60 s freshness | The moment after a follow is when suggestions work best. |
| Nightly refresh for every active user within 6 hours cost | Bounded by the batch cluster we can afford. |
| Zero suggestions of blocked, opted-out or already-followed accounts trust | Each one is a visible, trust-damaging bug, so filters run at serving time too. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Friends of friends, naively | 400 M active users × 200 follows × 200 follows ≈ 16 trillion two-hop paths a day, and far more through celebrities | → so we prune: only each user's strongest ties, and never walk through huge accounts |
| After pruning | 400 M × 30 strongest ties × 30 ≈ 360 B paths, combined per (user, candidate) pair | A large but ordinary batch join → so the nightly job runs on a Spark cluster in a few hours |
| Stored suggestions | 400 M users × 300 candidates × ~16 bytes ≈ 2 TB | → so precomputed lists live in a key-value store, one row per user |
| Serving | 2,000 requests/s × 1 row read | Small → so serving is cheap; spend the budget on filtering and a light re-rank |
Illustrative numbers. Play with the pruning in the lab in section 13.
6. The nouns
- Candidate: a (user, suggested account) pair with the sources that produced it.
- Features: facts about the pair: mutual follows, shared contacts, same workplace, how active the candidate is, how often they follow back.
- Score: the model's estimate that the user will follow and keep following.
- Suggestion list: the top few hundred scored candidates per user, with the reason to show ("Followed by Chen and 4 others").
- Exclusions: blocked, dismissed, already followed, opted out.
suggestions user_id → [(candidate_id, score, reason_code, sources)] × 300, computed_at key-value store
fresh user_id → [(candidate_id, score, reason_code)] × 50, expires in 24 h key-value store (near-line)
dismissed (user_id, candidate_id) → dismissed_at key-value store
contacts hash(normalised phone or email) → [user_ids who uploaded it] only with consent
discoverable user_id → {by_phone, by_email} settings
7. The contract
GET /v1/suggestions?placement=home&limit=20 → [{user, reason: "Followed by Chen and 4 others"}], request_id
POST /v1/suggestions/{candidate}:dismiss → 204
POST /v1/contacts:upload {hashes[]} (opt-in) → 202
Events consumed: Followed, Unfollowed, Blocked, ContactsUploaded, ProfileViewed
Events produced: SuggestionShown, SuggestionFollowed, SuggestionDismissed (training data)
The reason string is chosen from public facts only (mutual follows, a shared workplace the candidate made public). If the only link between two people is private, they aren't suggested at all.
8. First working version
Three paths. A nightly batch computes a long list for every active user. A near-line stream job adds fresh candidates after an action. A small serving service merges both, filters and returns the top of the list.
8.1 The nightly batch
8.2 Serving a request
9. Make it hold up
9.1 Taming the two-hop explosion cost
The candidate generators are separate and each is cheap: two-hop over strong ties, contacts (a join on hashed identifiers), shared workplace or school (an inverted index from workplace to members, sampled), and "people who view your profile or reply to you". Each contributes a few hundred candidates; the ranking model decides between them.
9.2 Fresh after a follow freshness
9.3 A brand-new user cold start
- Contacts, with consent. The app hashes normalised phone numbers and emails on the device and uploads the hashes; we match them to accounts that allow being found that way. This is the single strongest early signal.
- Sign-up context: who invited them (and that person's network), the language and country, interests picked during onboarding.
- Popular and local accounts as a fallback, chosen per country and interest.
- Then the near-line path takes over: the first few follows produce a two-hop neighbourhood within a minute.
9.4 Ranking: what to optimise quality
| Approach | Why | |
|---|---|---|
| ✗ Bad | Sort by number of mutual follows | A good first feature, but it ranks celebrities and spam rings highly and ignores everything else we know. |
| ~ Good | A model predicting "will follow" from pair features | Much better, but it learns to suggest accounts people tap on and later regret. |
| ✓ Great | Predict several outcomes (follow, follow-back or accept, still following after 30 days, dismiss, block) and combine them with weights the product team owns; train on logged suggestions, including the ones not followed | Rewards connections that last; a block or report costs far more than a follow gains. Online A/B tests check follow rate, follow-back rate and complaint rate together. |
A gradient-boosted tree over a few hundred features is a solid start (Module 29 covers serving models). The batch scores everything nightly; the serving path re-scores only the top 100 with fresh features.
9.5 Privacy: suggestions that leak trust
- Contacts are one-directional evidence. If only you have my number, that says nothing about whether I want to find you. Respect each person's "let people find me by phone or email" setting, and be careful with matches that would reveal who has whose number (a patient suggested to a doctor).
- Every suggestion needs a public reason. If the only link is private (an uploaded contact, a profile view), either show a vague reason or don't suggest.
- Blocks are absolute, in both directions, and checked at serving time as well as in the batch, because a block made an hour ago isn't in last night's lists.
- Sensitive groups: some product decisions exclude whole signal types (location, health-related groups). The pipeline enforces them centrally, not per model.
- Deletion: a deleted account is removed from every stored list and from the next training data.
9.6 Feedback loops quality
Suggestions create follows, which create more suggestions: popular accounts get more popular, and the model learns mostly from what it already chose to show. Keep a small share of exploration (candidates the model is unsure about), log the position each suggestion was shown in so training can correct for it, and watch the share of follows going to the top 1% of accounts. Spammers also try to get suggested: candidates with sudden follower growth or many dismissals are demoted until reviewed.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
Why not compute suggestions on request?
A two-hop walk with scoring for hundreds of candidates takes seconds and hammers the graph service. Precompute the long list; on request, only merge, filter and re-rank the top.
The nightly job fails.
Serve yesterday's lists (they're still mostly right) plus the near-line additions. The job writes a new version and switches atomically, so a half-written run is never served.
How do you test a new model?
Offline first: does it rank the follows that actually happened next week near the top (recall at 20)? Then an A/B test on a few percent of users, watching follows, follow-backs, dismissals and blocks, and long-term activity.
A user dismisses a suggestion.
Write it to the dismissed table (read at serving time, so it disappears at once), log it as a negative training example, and don't suggest that person again for months.
Two-way friendships instead of follows?
The same pipeline, but the other side must accept, so the model also predicts acceptance, and we cap how many requests a person can receive from suggestions so popular people aren't flooded.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Nightly batch late or failed | Slightly stale suggestions | List age per user | Serve the previous version; near-line still runs |
| Near-line stream behind | No fresh suggestions after a follow | Consumer lag | Fall back to batch lists |
| Bad model pushed | Odd suggestions; more dismissals | Dismiss and block rates by model version | Canary per model version; one-click rollback |
| Exclusion check down | Risk of suggesting blocked users | Errors on the block lookup | Fail closed: return fewer, safe suggestions (or none) |
| Spam ring gaming suggestions | Spam accounts suggested | Follower growth anomalies, dismissal spikes | Demote suspicious candidates; trust and safety review |
12. Wrap-up and the clock
Trade-offs we made:
- Precompute in batch, refresh near-line, filter and lightly re-rank at serving time.
- Prune the graph walk (strong ties only, no walking through huge accounts), accepting that a few weak candidates are missed.
- Several candidate sources, one ranking model optimising for lasting connections.
- Privacy rules and blocks enforced centrally and again at serving time; fail closed.
What we'd watch: follow rate and follow-back rate from suggestions · dismiss and block rates · list age · near-line lag · share of follows to the top 1% of accounts · batch job duration and cost.
At 10×: the batch grows with active users × ties², so pruning thresholds tighten, or the two-hop computation moves to incremental updates (only users whose neighbourhood changed).
A 60-minute interview
| 0–5 | Frame: placements, success metric, freshness, signals |
| 5–10 | Must do, how well, the explosion in numbers |
| 10–15 | Nouns and contract |
| 15–25 | Batch, near-line and serving paths |
| 25–50 | Deep dives: pruning (9.1), freshness (9.2), ranking (9.4) |
| 50–60 | Privacy, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including signals we may not use |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, contract |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: failures, testing, dismissals |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Friends of friends counted by mutual follows, a precomputed list per user, filtering out existing follows. |
| Senior | The explosion quantified and pruned, several candidate sources, a batch plus near-line split, a model with sensible labels, serving-time exclusions. |
| Staff+ | Privacy leaks through suggestions and their reasons, optimising for lasting connections rather than taps, feedback loops and exploration, failure modes that fail closed. |
Common mistakes:
- A full two-hop walk on every request.
- Counting paths through celebrities.
- Only a nightly batch, so the list ignores what the user did a minute ago.
- Training on clicks alone.
- Forgetting that a block made after the batch must still apply.
- Ignoring what a suggestion reveals.
13. Try it, check yourself, read more
The two-hop explosion lab. Choose how many ties each user keeps and the largest account allowed in the middle of a path, and see the size of the nightly job.
Illustrative model: follows per user and the share of paths through large accounts come from a made-up but typical skewed distribution; a cluster joins ~2 billion path records a second.
How do you keep the friends-of-friends job affordable?
A user just followed a colleague. How do suggestions reflect that?
What should the ranking model optimise?
A user blocked someone an hour ago, after last night's batch. What guarantees that person isn't suggested?
| Resource | Why |
|---|---|
| WTF: The Who to Follow Service at Twitter (WWW 2013) | A production design: graph algorithms over an in-memory copy of the follow graph. |
| Spark GraphX guide | Graph computations as batch jobs. |
| Module 23: batch and stream processing | The batch plus near-line split used here. |