InterviewKata
System design · Worked design D40 of 46 · Social and content · Long form

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:

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

  1. Return a ranked list of suggested accounts for a user, quickly, for each placement.
  2. Find candidates from several sources: friends of friends, contacts, shared school or workplace, people who interact with the user.
  3. React to new actions: a follow, a dismissal or a contact upload changes the next list.
  4. 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

TargetWhy this number
Serving under 100 ms at p99 speedThe carousel loads with the home screen.
Fresh after an action within 60 s freshnessThe moment after a follow is when suggestions work best.
Nightly refresh for every active user within 6 hours costBounded by the batch cluster we can afford.
Zero suggestions of blocked, opted-out or already-followed accounts trustEach one is a visible, trust-damaging bug, so filters run at serving time too.

5. Numbers that change the design

QuantityWorking→ so we…
Friends of friends, naively400 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 pruning400 M × 30 strongest ties × 30 ≈ 360 B paths, combined per (user, candidate) pairA large but ordinary batch join → so the nightly job runs on a Spark cluster in a few hours
Stored suggestions400 M users × 300 candidates × ~16 bytes ≈ 2 TB→ so precomputed lists live in a key-value store, one row per user
Serving2,000 requests/s × 1 row readSmall → 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

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

Parked for latertaming the two-hop explosion (9.1) · freshness after a follow (9.2) · new users (9.3) · ranking and what to optimise (9.4) · privacy (9.5) · feedback loops (9.6).

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

9.4 Ranking: what to optimise quality

ApproachWhy
✗ BadSort by number of mutual followsA good first feature, but it ranks celebrities and spam rings highly and ignores everything else we know.
~ GoodA model predicting "will follow" from pair featuresMuch better, but it learns to suggest accounts people tap on and later regret.
✓ GreatPredict 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 followedRewards 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

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

FailureWhat users seeHow we detect itWhat limits the damage
Nightly batch late or failedSlightly stale suggestionsList age per userServe the previous version; near-line still runs
Near-line stream behindNo fresh suggestions after a followConsumer lagFall back to batch lists
Bad model pushedOdd suggestions; more dismissalsDismiss and block rates by model versionCanary per model version; one-click rollback
Exclusion check downRisk of suggesting blocked usersErrors on the block lookupFail closed: return fewer, safe suggestions (or none)
Spam ring gaming suggestionsSpam accounts suggestedFollower growth anomalies, dismissal spikesDemote suspicious candidates; trust and safety review

12. Wrap-up and the clock

Trade-offs we made:

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–5Frame: placements, success metric, freshness, signals
5–10Must do, how well, the explosion in numbers
10–15Nouns and contract
15–25Batch, near-line and serving paths
25–50Deep dives: pruning (9.1), freshness (9.2), ranking (9.4)
50–60Privacy, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including signals we may not use
8–15Must do, how well, numbers
15–22Nouns, contract
22–35First working version
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: failures, testing, dismissals
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelFriends of friends counted by mutual follows, a precomputed list per user, filtering out existing follows.
SeniorThe 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:

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.

Active users
Ties kept per user (first hop)
Skip middle accounts above

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?

Celebrities add paths but almost no signal; weak ties add paths but little signal.

A user just followed a colleague. How do suggestions reflect that?

Batch for breadth, near-line for the moment after an action.

What should the ranking model optimise?

Taps reward regretted follows; the product wants connections that last.

A user blocked someone an hour ago, after last night's batch. What guarantees that person isn't suggested?

Precomputed lists are stale by design; trust rules are enforced live.
ResourceWhy
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 guideGraph computations as batch jobs.
Module 23: batch and stream processingThe batch plus near-line split used here.