Skip to content
System design course
Ch.4 · Designing real systems·how to build it ·7 min read

Building a news feed (Facebook)

Implement the candidate-generation-then-rank pipeline, the engagement-prediction scorer with a feature store, and incremental re-ranking.


The read pipeline: generate → rank → hydrate

def news_feed(user_id, k=20):
    candidates = gather_candidates(user_id, n=500)   # fan-out feed + groups/pages
    candidates = cheap_filter(candidates)            # dedup, drop seen/blocked → ~300
    features   = feature_store.batch(user_id, candidates)   # author affinity, recency, popularity...
    scored     = ranking_model.predict(features)     # P(engage) per candidate
    ranked     = sort_desc(scored)
    ranked     = interleave_ads(ranked, ad_auction(user_id))
    return hydrate(ranked[:k])                        # post content + CDN media

The structure is candidate generation (recall) → ML ranking (precision) → hydrate, the same two-stage shape as the search engine.

Candidate generation

Reuse fan-out feeds for friends, and pull recent posts from followed pages/groups:

def gather_candidates(user_id, n):
    feed_ids   = redis.lrange(f"feed:{user_id}", 0, n)        # pushed friend posts
    page_ids   = recent_posts(pages_followed(user_id), n)      # pulled page/group posts
    return dedupe(feed_ids + page_ids)

The ranking model

Score predicted engagement from features; in practice a gradient-boosted tree or neural net, but conceptually:

def score(features):
    # learned weights over: author affinity, post weight (type/eng), recency decay,
    # user-topic match, post popularity, time-of-day, ...
    return model.predict(features)          # P(like|comment|share|dwell)

A feature store serves these features at low latency (precomputed affinities, rolling engagement counts, user embeddings). Keeping feature fetch fast is what makes read-time ranking viable.

Incremental re-ranking and freshness

  • Rolling engagement counts (likes/comments in the last hour) update a post’s features in near-real-time (stream processing), so a post gaining traction climbs.
  • Negative feedback (hide, “see less”) immediately down-ranks similar content and trains the model.
  • Cache a user’s ranked feed briefly (seconds–minutes) to absorb rapid refreshes, but re-rank on meaningful new activity.

Hydration and serving

Ranked post ids are hydrated into full posts (batched cache/DB reads), with media URLs pointing at the CDN at the right size (Instagram pattern). Pagination continues the ranking from where it left off (cursor).

Storage and scale

  • Candidate feeds in Redis (fan-out), posts sharded, media on CDN.
  • Feature store (Redis/specialized) for low-latency features.
  • Ranking service is a stateless, horizontally-scaled fleet behind the read path; the heavy model runs only on the filtered candidate set.

Failure handling

  • Ranking service down/slow → fall back to chronological order (degrade gracefully — a feed in time order beats no feed).
  • Feature store miss → score with defaults; don’t block the feed.
  • Celebrity/page with huge reach → pulled at read (no fan-out storm).
  • Stale counts → acceptable; eventual consistency.

The takeaway

Concrete signals: two-stage candidate-generation-then-ML-rank, a low-latency feature store, stream-updated engagement features for freshness, graceful fallback to chronological, and CDN hydration. Fan-out gives you what could appear; the ranking pipeline decides what does — the reusable recipe for any ranked feed (TikTok’s “For You” is the same shape with stronger ML).