Build a Follow Recommendation Engine Using Graph Traversal


data-engineering distributed-systems scalability

System Design Deep Dive

Follow Recommendation Engine

Walking a 500-million-node social graph to find your next follow, without ever traversing all of it.

⏱ 16 min read 📐 Advanced 🏗️ Data Engineering

Picture the host of a large industry conference who is genuinely good at introductions. She does not hand you a directory of every attendee, and she does not just introduce you to whoever happens to be standing closest. She thinks about who you already know, glances at who those people know, and introduces you to the handful of strangers you are two handshakes away from, favoring the ones several of your acquaintances already know well over the one your colleague met once at the bar. That is a second-degree introduction, and it works as a heuristic because a friend of a friend, especially one several friends have in common, is a far better bet than a total stranger picked at random.

Now replace the host with a recommendation engine, replace a room of a few hundred attendees with a social graph of 500 million nodes, and replace a handful of quiet introductions with a “who to follow” widget that has to compute a fresh short list for hundreds of millions of people every single day, without ever making a single user wait more than a few hundred milliseconds to see it. The engineering problem is exactly the one the host solves, but at a scale where “look at who your friends know” stops being a quick mental exercise and becomes a distributed traversal over tens of billions of edges, some of which belong to accounts so widely followed that expanding their neighbor list even once could touch millions of rows.

The naive approach, run a live breadth-first search over the follow graph the instant a user opens the app, fails for a boring but decisive reason: an ordinary account follows on the order of a hundred people, but the graph’s degree distribution is a power law, and a meaningful fraction of any user’s friend-of-friend walk passes through at least one hub account with an out-degree or in-degree in the millions. Expanding that one node fully turns a bounded, cheap traversal into an unbounded, slow one, and it happens often enough that “just run BFS on read” produces unpredictable, frequently unacceptable latency. The opposite naive approach, precompute nothing and always fall back to a simple popularity list, avoids the latency problem entirely but throws away the entire reason a graph-based recommendation is good in the first place: it stops being personalized to the one graph that is actually relevant, yours.

We need to solve for three things simultaneously: a traversal strategy that can walk two hops out from every one of 500 million users without letting a single hub account blow up the cost of that walk, a pruning and scoring pipeline that turns tens of thousands of raw second-degree candidates into a short, meaningfully ranked list cheaply enough to run for hundreds of millions of users a day, and a clean separation between the steady-state case, where a user already has an established graph and yesterday’s precomputed answer is good enough, and the cold-start case, where a brand-new account has no graph signal to traverse at all.

Requirements and Constraints

Functional Requirements

  • Ingest follow and unfollow edges into a partitioned graph store as they happen, keeping both a forward adjacency (who I follow) and a reverse adjacency (who follows me) available for downstream use
  • For every eligible user, traverse outward to second-degree connections, friends of friends, bounded by a fan-out cap, and produce a shortlist of candidates the user does not already follow
  • Score each surviving candidate primarily by mutual connection count, blended with a decayed activity signal, so a candidate who shares ten mutual friends but has not logged in for a year does not automatically outrank one with six mutual friends who is active daily
  • Prune candidates aggressively before scoring: exclude already-followed accounts, blocked or muted accounts, previously dismissed candidates, and anyone below a minimum mutual-connection threshold
  • Recompute the full recommendation list for every eligible user on a daily cadence via an offline batch job, rather than on every read
  • Serve a ranked “who to follow” list on demand from the precomputed output through a low-latency read path
  • Handle new users who have too few, or zero, graph edges to produce a meaningful second-degree traversal, through a distinct cold-start strategy that does not depend on the batch job having ever run for them
  • Let a user dismiss or hide a recommended candidate, and never resurface that exact candidate again for that user

Non-Functional Requirements

  • Scale: 500 million total user nodes in the social graph, average out-degree (accounts followed) of roughly 120, producing around 60 billion directed follow edges stored forward, mirrored by a reverse index of comparable size
  • Batch throughput: the nightly refresh covers roughly 220 million users who were active in the last 30 days, and the full run must complete inside a bounded nightly compute window, targeted at 4 hours
  • Read latency: the “who to follow” widget’s precomputed read path targets a p99 under 150ms
  • Cold-start latency: the on-demand fallback for a user with no batch-computed row must respond within roughly 300ms, since it is served synchronously on request rather than precomputed
  • Freshness: a batch-computed recommendation list may lag the live graph by up to 24 hours; a follow made five minutes ago does not need to instantly change someone else’s recommendations, but a suppression, a user dismissing a candidate, must take effect immediately, not on the next nightly run
  • Availability: the online read path targets 99.9% uptime; the graph write path (the follow/unfollow feature itself) must never be blocked, throttled, or slowed by the batch job’s read load against the same storage layer
  • Bounded fan-out: no single user’s traversal may expand a hub node’s full neighbor list; fan-out per hop must stay bounded regardless of how large any individual node’s true degree is
Key Insight

The daily refresh cadence is a deliberate cost decision, not a technical ceiling. The traversal and scoring pipeline could technically run more often, but the marginal value of running it every few hours instead of once a day is small, since a person's social graph rarely shifts enough in that window to change who their best next follow actually is. Spending compute on cheaper, always-fresh signals, like instantly honoring a dismissal, buys far more perceived quality per dollar than spending it on shortening the batch interval.

Constraints and Assumptions

  • Turn-by-turn UI rendering of the “who to follow” widget, push notifications, and any A/B experimentation framework around recommendation placement are out of scope; this design only decides who to suggest and in what order
  • The follow/unfollow feature itself, meaning the write path that lets a user actually follow someone, is owned by a separate profile and graph-edge service; this design treats it as an upstream event stream, not something we build here
  • We assume the graph is stored in a distributed, partitioned adjacency-list store, comparable to a wide-column store; we design its schema and partitioning key, not the storage engine itself
  • We assume a separate content and interest classification service exists and can supply an interest-tag signal per user, used only by the cold-start fallback, never by the primary mutual-friend scoring path
  • We treat account-level moderation (spam detection, ban lists) as an upstream signal this system consumes through the blocked and muted sets, not something it computes

High-Level Architecture

The system has five major components: the Graph Ingestion and Partitioned Storage Layer, the Bounded BFS Traversal Engine, the Candidate Pruning and Mutual-Connection Scorer, the Offline Batch Recommendation Pipeline, and the Online Serving Layer and Cold-Start Handler.

Architecture overview showing follow event ingestion into a partitioned edge store, a nightly batch pipeline running bounded BFS traversal and candidate pruning and scoring, a recommendation store, and an online serving layer with a cold-start fallback path

The Graph Ingestion and Partitioned Storage Layer is the front door for every follow and unfollow action: it validates the event and writes it into a sharded edge store that both the live write path and the nightly batch job can read without contention. The Bounded BFS Traversal Engine is the computational core, run once per eligible user per night, walking two hops out from that user’s own follows with a hard cap on how many edges it will expand at each hop. The Candidate Pruning and Mutual-Connection Scorer takes the raw, noisy set of second-degree candidates that traversal produces and turns it into a short, ranked list by excluding the obviously irrelevant candidates cheaply before spending anything on expensive per-candidate scoring. The Offline Batch Recommendation Pipeline is the orchestration layer that runs the traversal and scoring steps across the entire eligible user population once a night and materializes the results. The Online Serving Layer and Cold-Start Handler is what actually answers a “who to follow” request: it reads the precomputed result when one exists, applies any recommendation-time filtering that cannot wait for tomorrow’s batch, and falls back to a popularity-based strategy for any user the batch has no meaningful signal for.

A recommendation’s life looks like this: a user follows or unfollows someone, and that edge lands in the partitioned store immediately, available to the write path and to tomorrow’s batch run, but not retroactively applied to today’s already-computed recommendations. Once a night, the orchestrator hands the eligible user population to the traversal engine, which walks each user’s graph out to two hops, capping fan-out at every hub it encounters along the way. The pruning and scoring stage strips out anything already followed, blocked, muted, or previously dismissed, then ranks what remains by a blend of mutual connection count and recent activity, and the top twenty candidates per user are written to the recommendation store. When a client actually opens the “who to follow” widget, the online serving layer reads that precomputed row, filters out anything the user has dismissed since the batch ran, and returns it, unless the user is new enough or thin-graphed enough that no meaningful precomputed row exists, in which case the request is answered from the cold-start popularity index instead.

Key Insight

The single most important architectural decision is keeping the expensive part of this system, graph traversal and scoring, entirely offline and decoupled from the read path. The online serving layer never touches the graph at all; it only ever reads a precomputed row and applies a cheap filter. That split is what lets the read path hit a 150ms `p99` against a system whose write side is walking tens of billions of edges every night.

Component Deep Dives

The Graph Ingestion and Partitioned Storage Layer

This component’s job is to turn a raw follow or unfollow event into a partitioned adjacency structure that both the live write path and the nightly batch job can use without contending with each other.

The obvious mistake is storing only one direction of the relationship. A recommendation engine built purely on “who does this user follow” can answer “who are my friends” cheaply, but the moment you need “who follows this candidate account” for popularity ranking, hub detection, or a future feature like showing mutual followers, you are stuck scanning the entire forward table for a match. We store both a forward index, keyed by follower_id, and a reverse index, keyed by followee_id, but we are careful to keep the primary second-degree traversal walking only the forward index. The reverse index exists to serve the cold-start popularity computation and hub-detection tooling, not the hot traversal path.

Premium Content

Unlock the full article along with everything else in the archive — all in one place.

In-depth analysis Expert insights Full archive access
Unlock Full Article