# System Design: Feed Ranking and Personalization — Candidate Generation, Scoring, and Real-Time Updates (2025)

Source: https://www.techinterview.org/post/3233466385/system-design-feed-ranking/
Updated: 2026-04-17 · techinterview.org

## Feed Architecture Overview

A social feed shows users content from people and pages they follow, ranked by predicted engagement. The pipeline has three stages: **Candidate generation**: retrieve the pool of candidate posts (from follows, interest graphs, trending topics). **Ranking**: score each candidate with an ML model predicting user engagement (click, like, share, comment, dwell time). **Filtering and serving**: apply business rules (diversity, safety, deduplication), truncate to top N, serve. At Instagram/TikTok scale: billions of posts, hundreds of millions of users, < 200ms feed load SLA. The key trade-off: coverage (seeing all relevant content) vs. latency vs. freshness.

## Fan-Out Strategies: Push vs. Pull

**Fan-out on write (push model)**: when a user posts, push the post ID to the feed inbox of all followers. Read: just read your pre-computed inbox. Write: O(followers) — expensive for celebrities (Cristiano Ronaldo has 600M followers). **Fan-out on read (pull model)**: on feed load, fetch posts from all accounts the user follows, merge and rank. Read: O(accounts_followed * posts_per_account) — expensive for users following thousands. **Hybrid (Instagram's approach)**: For users with  10K followers): fan-out on read — inject celebrity posts at feed serving time. This caps write amplification while keeping reads fast for most users. Celebrity threshold is a tunable parameter.

## Candidate Generation


```
class CandidateGenerator:
    def get_candidates(self, user_id: int, limit: int = 500) -> list[Post]:
        candidates = []

        # 1. Follow graph: posts from accounts the user follows
        follow_posts = self.follow_feed_store.get_recent(
            user_id, limit=200
        )  # Redis sorted set: ZREVRANGE feed:{user_id} 0 199

        # 2. Interest graph: posts from topics the user engages with
        interest_posts = self.interest_store.get_recommended(
            user_id, limit=150
        )  # Embedding similarity in vector DB

        # 3. Trending: globally trending content
        trending = self.trending_store.get_trending(
            region=user.region, limit=50
        )

        # 4. Re-engagement: posts the user liked/shared that have new replies
        reengagement = self.reengagement_store.get(user_id, limit=50)

        # Deduplicate by post_id
        seen = set()
        for post in follow_posts + interest_posts + trending + reengagement:
            if post.id not in seen:
                candidates.append(post)
                seen.add(post.id)

        return candidates[:limit]
```


## Ranking Model

Features for the ranking model: **Post features**: content type (video/photo/text), post age, creator follower count, post engagement velocity (likes per hour), content embeddings. **User features**: historical engagement with this creator, content type preference, session context (time of day, device), recent interaction history. **Cross features**: user-creator affinity score, user-content-type affinity. Model: two-tower neural network (user tower + post tower → dot product for relevance score) or gradient-boosted trees for multi-task prediction (predict P(click), P(like), P(share), P(dwell > 10s) separately, combine into a single score). Multi-task loss prevents optimizing only for clicks (click-bait) — weight the combination based on business values.

## Real-Time Feed Updates and Freshness

New posts should appear in feeds within seconds of publishing. Real-time injection: when a post is created, publish to a Kafka topic (new_posts). Feed service consumers process new_posts events and inject them into Redis sorted sets (fan-out on write). For pull-model users: the injection is skipped — the post is available for pull on next feed load. **Freshness signals**: recently posted content gets a "freshness boost" in the ranking score that decays over time (exponential decay: score * exp(-lambda * age_hours)). This ensures new posts from followed accounts surface even against older high-engagement posts. **Seen-post filtering**: track posts already shown to the user in a Bloom filter (per user, per session). Filter candidates against the Bloom filter before ranking to avoid showing the same post twice in consecutive feed loads.

See also: [Meta Interview Prep](https://techinterview.org/meta/)

See also: [Twitter/X Interview Prep](https://techinterview.org/twitter/)

See also: [LinkedIn Interview Prep](https://techinterview.org/linkedin/)
