IntervueClubbeta
Continue where you left off?

Reading long content?

Light mode reduces eye strain for sustained reading sessions.

🧠 You've been reading for 20 minutes

A short break now helps your brain consolidate what you just read. You'll retain more.

🌙 Reading late?

Your brain absorbs less after midnight. Consider bookmarking this and coming back fresh — you'll get more out of it.

/ to search·TOC on the right to jump sections·dark/light toggle in nav

Design a News Feed

hardDatabasesfan-outcachingsocial-graphpaginationrankingkafkaredis
Jun 12, 2026·~23 min read
Asked atMetaTwitterLinkedInInstagramTikTokSnap
Watch Video Walkthrough
Watch the author walk through the problem step-by-step

Problem Statement#

"Design a news feed system. Users should be able to post content and see a feed of posts from people they follow."

That's it. One sentence. The rest is yours to figure out.


Requirements Gathering#

This is where the interview actually begins. A strong candidate doesn't start drawing boxes. They ask questions. Not to stall, but because the answers fundamentally change the design.

Here's how a good requirements conversation looks:


Candidate: "Is this a mutual follow system like Facebook friends, or one-directional like Twitter where I can follow someone who doesn't follow me back?"

Interviewer: "One-directional. Think Twitter or Instagram."


Candidate: "Should the feed be purely reverse-chronological, or ranked by relevance, engagement, relationship strength, that kind of thing?"

Interviewer: "Ranked. Recency matters but it's not the only signal."


Candidate: "What scale are we designing for? DAU ballpark?"

Interviewer: "500 million DAU. About 300 million new posts per day."


Candidate: "How fresh does the feed need to be? If someone I follow posts right now, how quickly should it appear on my feed?"

Interviewer: "Within a few seconds. Near real-time."


Candidate: "Are we handling celebrity accounts, people with tens of millions of followers? That changes the write architecture significantly."

Interviewer: "Yes, the system should handle that."


Candidate: "What types of content, text only, or images and videos too?"

Interviewer: "Text, images, links. No video for now."


Candidate: "Infinite scroll, so we need pagination. Any requirement on how far back the feed goes?"

Interviewer: "Show the last 500 posts or 7 days, whichever comes first."


Candidate: "Out of scope: Stories, Ads, content from people you don't follow, Explore and Reels-style recommendations?"

Interviewer: "Correct, out of scope. Just the social graph feed."


Functional and Non-Functional Requirements#

Functional Requirements#

  1. Users can create posts (text, images, links)
  2. Users can follow and unfollow other users
  3. Users see a ranked feed of posts from people they follow
  4. Feed supports infinite scroll with stable pagination
  5. New posts appear in followers' feeds within a few seconds
  6. Users can like and comment on posts

Non-Functional Requirements#

RequirementTarget
Feed load latencyunder 200ms p99
Post-to-feed freshnessunder 5 seconds
Availability99.99%
Consistency modelEventual (feed staleness is acceptable)
Scale500M DAU, 300M posts/day

Out of scope: Stories, Ads, Explore/Recommendations, Video streaming, DMs

Capacity Estimates#

Work through the math. Don't just state the numbers.

code
Post creation:
  300M posts/day ÷ 86,400 sec = ~3,500 posts/sec

Feed reads (users check feed ~5x/day):
  500M DAU × 5 ÷ 86,400 = ~29,000 feed reads/sec

Fan-out volume (avg 300 followers per user):
  3,500 posts/sec × 300 followers = ~1,050,000 feed updates/sec

Storage (post metadata only, avg 1KB per post):
  300M × 1KB = 300GB/day

Feed cache (500 entries/user; a sorted-set entry is NOT 16 bytes):
  Raw data is ~16B (8B post_id + 8B score), but a Redis ZSET
  element carries skiplist + hashtable + SDS overhead ≈ 60-90B.
  500M users × 500 × ~80B = ~20TB Redis memory
  Across ~160 cluster nodes: ~125GB/node
  (This is why you cap at 500 and only fan out to ACTIVE users:
   the memory bill, not CPU, is the binding constraint.)

The fan-out number, roughly 1M updates/sec, is the number that should make you pause. That's the core challenge.


High-Level Design#

Two flows drive everything: writing a post and reading the feed. Keep them separate in your head.

Write path: Client → Load Balancer → Post Service → saves to Post DB → publishes event to Kafka → Fan-out Service consumes → looks up followers from Social Graph DB → writes post ID to each follower's feed in Redis.

Read path: Client → Load Balancer → Feed Service → reads post IDs from Redis feed cache → batch-fetches post content from Post Cache → returns assembled feed.

The key insight: reads never touch the Post DB directly. Feed reads are a Redis lookup followed by a batch cache fetch. The database is only involved in writes.


Deep Dives#

1. The Fan-out Problem: Push vs Pull vs Hybrid#

This is the central question of the entire interview. Get this right and you've won most of the points.

Fan-out on write (push model)

When User A posts, the fan-out service immediately writes that post ID into the Redis feed of every follower.

  • Feed reads become O(1) Redis lookups, extremely fast
  • No computation at read time
  • Problem: a user with 10M followers posting means 10M Redis writes triggered synchronously. At 3,500 posts/sec from such users, the fan-out queue drowns.

Fan-out on read (pull model)

When a user opens their feed, the system queries recent posts from everyone they follow, merges and ranks them on the fly.

  • One write per post regardless of follower count
  • Problem: a user following 500 accounts means 500 database queries per feed load, then merge and rank. At 29,000 feed loads/sec this is catastrophic.

Hybrid approach (what Twitter and Instagram actually use)

Fan-out on write for regular users (followers below a threshold, e.g. 10,000). Fan-out on read for celebrities (followers above threshold).

At read time: merge the pre-computed feed from Redis (regular followees) with a real-time pull from celebrity accounts you follow.

code
if follower_count < CELEBRITY_THRESHOLD:
    # Push post_id to each follower's Redis sorted set
    fan_out_to_followers(post_id, followers)
else:
    # Store in celebrity post cache (shared, not per-follower)
    cache_celebrity_post(post_id, author_id)
    # Followers merge this at read time

Celebrity posts are cached in a shared sorted set keyed by celebrity:{user_id}, scored by post timestamp, not duplicated per follower. One cache entry serves all 10M followers. Extremely cache-efficient.

The score matters: at read time you merge celebrity posts into the same timeline as the pre-computed feed, so celebrity entries must carry a timestamp to sort and to respect the pagination cursor. (Post IDs are Snowflake IDs, so the timestamp is also recoverable from the ID itself, but storing it as the sorted-set score keeps the read path a single ranged query instead of a decode-and-sort.)

For senior interviews ↓ threshold tuning and partial fan-out

The 10,000 follower threshold isn't magic. In practice, teams A/B test it. The real question is: what's your worst-case fan-out latency budget?

Think in terms of write amplification, not wall-clock time (a single post never gets the whole cluster's write budget; that budget is shared across every post in flight). One post from a 500K-follower account is 500K individual ZADDs; from a 50M-follower account it's 50M. Even spread across many fan-out workers and shards, the 50M case is orders of magnitude more work than the 3,500 posts/sec baseline can absorb, and the backlog is what makes celebrity posts propagate slowly. That amplification, not any single latency number, is why celebrities skip fan-out entirely.

A more nuanced approach used in production is partial fan-out. Instead of fanning out to all followers, only fan out to followers who were active in the last 7 days. Studies show 80-90% of followers of any account are inactive at any given time. This reduces effective fan-out by 5-10x with zero impact on active user experience.

Twitter used to do full fan-out on write for all users, which is why celebrity tweets were historically slow to propagate. After the celebrity problem became acute with accounts like Katy Perry and Barack Obama, they moved to hybrid.


2. Feed Storage: Redis Sorted Sets#

The feed cache is not a simple list. It needs to support ordered retrieval (by timestamp or ranking score), cursor-based pagination, O(log N) inserts, and efficient trimming to keep only the last 500 entries.

Redis Sorted Set is exactly the right structure.

code
Key:    feed:{user_id}
Member: post_id
Score:  timestamp (unix milliseconds) OR ranking score (float)
Max:    500 entries per user (ZREMRANGEBYRANK trims older ones)
code
# Fan-out worker: add post to follower's feed
ZADD feed:user_123 1718000000000 post_abc
ZREMRANGEBYRANK feed:user_123 0 -501   # keep latest 500

# Feed read: latest 20 posts
ZREVRANGE feed:user_123 0 19 WITHSCORES

# Cursor pagination: posts older than cursor_score
ZREVRANGEBYSCORE feed:user_123 (cursor_score -inf LIMIT 0 20

The ( prefix on cursor_score means exclusive. The post at the cursor is not returned again.


3. Feed Ranking#

Reverse-chronological is the baseline. Real ranking uses multiple signals scored together.

Signals:

  • Recency: newer posts score higher, with a decay curve rather than a hard cutoff
  • Engagement velocity: posts getting lots of likes/comments in the first 5 minutes get a boost
  • Affinity: posts from accounts you interact with frequently rank higher
  • Content type affinity: if you engage more with images, image posts rank higher
  • Diversity: penalize showing 5 consecutive posts from the same author

In interviews, you don't need to design the full ML model. Name the signals, describe the two-pass approach.

Two-pass ranking:

  1. Write time (coarse): When inserting into the feed sorted set, compute a basic score using recency plus the author's engagement history. Store as the sorted set score.
  2. Read time (fine): Pull 100-200 candidate posts from Redis, re-score them with fresh engagement signals (like counts updated in the last few minutes), return top 20.

This separates feed assembly (fast, Redis) from ranking (slightly slower, can be a separate service).

For senior interviews ↓ production ranking pipeline

At Facebook and Instagram scale, ranking is a dedicated ML service. The feed pipeline looks like:

  1. Candidate retrieval: pull roughly 500 candidate posts from Redis (fast, approximate)
  2. Light scoring: apply a fast scoring model, drop to about 100 candidates
  3. Heavy scoring: apply a deep ML model with personalization features
  4. Re-ranking: apply diversity rules, ad insertion, hard rules like not showing blocked users
  5. Serve

The feed service calls the ranking service as a synchronous dependency, so ranking latency is in the critical path. Production systems use model distillation and FPGA inference to keep heavy scoring under 20ms.

The heavy model is retrained daily. Light signals like like counts and recent engagement are updated in near-real-time from a streaming pipeline (Kafka → Flink → feature store).


4. Pagination: Cursor vs OFFSET#

This one comes up in almost every interview. Know it cold.

OFFSET-based pagination (wrong at scale):

SQL
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 200;

Two problems:

  1. The database scans and discards 200 rows on every request, O(N) cost per page
  2. If new posts arrive between page 1 and page 2, posts shift positions and users see duplicates or miss posts

Cursor-based pagination (correct):

The cursor is the score (timestamp) of the last post the client received. Next page means posts with score less than the cursor.

code
GET /feed?limit=20&cursor=1718000000000

Server: ZREVRANGEBYSCORE feed:{user_id} (1718000000000 -inf LIMIT 0 20

Response: {
  posts: [...],
  next_cursor: "1717999000000",
  has_more: true
}

The client stores next_cursor and sends it with the next scroll event. No position shifting. No duplicate posts. O(log N) query cost.


5. Post Hydration#

The feed cache stores post IDs only, not the full post content. This is intentional.

Why not store full posts? The same post appears in potentially millions of feeds. When the like count updates (which happens constantly), you'd need to update millions of Redis entries. Instead:

  1. Read 20 post IDs from feed:{user_id} sorted set
  2. Batch-fetch post content from a Post Cache (Redis): MGET post:abc post:def ...
  3. On cache miss, batch-fetch from Post DB
  4. Fetch author profiles from a separate User Cache
  5. Assemble and return

Post cache TTL: 24 hours. Hot posts naturally stay warm. Cold posts fall through to DB, which rarely happens.


Data Model#

SQL
-- Posts (Cassandra or Postgres, partitioned by author_id)
posts (
  post_id       BIGINT PRIMARY KEY,  -- Snowflake ID (time-ordered)
  author_id     BIGINT NOT NULL,
  content       TEXT,
  media_urls    JSONB,
  like_count    INT DEFAULT 0,
  comment_count INT DEFAULT 0,
  created_at    TIMESTAMPTZ DEFAULT NOW()
)
-- Index: (author_id, created_at DESC) for profile page + celebrity pull

-- Follows (Postgres, two indexes critical)
follows (
  follower_id   BIGINT,
  followee_id   BIGINT,
  created_at    TIMESTAMPTZ,
  PRIMARY KEY (follower_id, followee_id)
)
-- Index: (followee_id) for fan-out, "who follows this person?"
-- Index: (follower_id) for feed read, "who does this person follow?"

-- Feed Cache (Redis Sorted Set)
-- Key: feed:{user_id}
-- Member: post_id (string)
-- Score: created_at timestamp (float, milliseconds)
-- Max size: 500 entries, trim on insert

-- Celebrity Post Cache (Redis Sorted Set, shared across all followers)
-- Key: celebrity:{user_id}
-- Member: post_id (string)
-- Score: created_at timestamp (float, milliseconds), needed for merge + cursor
-- Max size: last 100 posts, trim on insert

API Design#

code
POST /api/v1/posts
Authorization: Bearer <token>
Body: { "content": "Hello world", "media_urls": [] }
Response 201: { "post_id": "...", "created_at": "..." }

---

GET /api/v1/feed?limit=20&cursor=<timestamp_ms>
Authorization: Bearer <token>
Response 200: {
  "posts": [
    {
      "id": "post_abc",
      "author": {
        "id": "user_123",
        "username": "alice",
        "avatar_url": "https://..."
      },
      "content": "Hello world",
      "media_urls": [],
      "like_count": 142,
      "is_liked_by_me": false,
      "created_at": "2026-06-07T10:00:00Z"
    }
  ],
  "next_cursor": "1718000000000",
  "has_more": true
}

Note: user_id is never sent by the client. Identity comes from the auth token and the server extracts it server-side. Sending user_id as a query param is a security mistake.


Failure Scenarios and Edge Cases#

For senior interviews ↓ what breaks and how the system recovers

Fan-out service goes down

Posts are still written to the Post DB. Fan-out events sit durably in Kafka and are not lost. When the fan-out service recovers, it processes the backlog from its last committed offset. Feeds are temporarily stale but no data is lost. Kafka's retention keeps events safe for 7 days.

Redis feed cache node fails

With Redis Cluster, the cluster promotes the replica for the failed shard automatically. During promotion (10-30 seconds), feeds for users on that shard return a cache miss. The Feed Service falls back to fan-out on read for that request and queries the Post DB directly. Slower, but the user sees a feed. Never return a blank page.

Cold start: new user or user returning after 30+ days

Their feed sorted set either doesn't exist or has expired (TTL = 30 days, reset on every fan-out insertion). On first load:

  1. Detect cache miss
  2. Fall back to fan-out on read: query Post DB for recent posts from followed accounts
  3. Seed the feed cache asynchronously so the next load is fast
  4. Return the on-the-fly computed feed for this one request

Thundering herd on celebrity cache miss

A celebrity posts. 100K concurrent users all request the feed at the same time. Celebrity cache is cold. Without protection, 100K requests hit Cassandra simultaneously.

Solution: use Redis SETNX as a distributed lock on celebrity_rebuild:{celebrity_id}. The first request acquires the lock, fetches from DB, and populates the cache. All other requests either wait briefly or serve a slightly reduced feed (without celebrity posts) during the lock period. Lock TTL: 5 seconds.

Fan-out queue lag

During peak hours (New Year's Eve, major news events), post creation spikes. Kafka consumer lag grows. Feeds become stale.

Mitigation: pre-provision Kafka partition count high enough to add consumers horizontally. Monitor consumer lag as a key SLO metric. If lag exceeds 30 seconds, page on-call.

User unfollows someone: stale posts in feed

Posts from the unfollowed user may still be in the feed sorted set. Options:

  • Hard delete: scan and remove from the sorted set immediately, expensive
  • Soft: next time the feed is assembled, filter out posts from unfollowed users by adding a bloom filter or a follows check at hydration time
  • Lazy: let TTL handle it and stale posts age out in 30 days

Most production systems use the soft approach. The cost of hard deletes at scale isn't worth it.

Post deletion

Post is deleted from Post DB. Feed sorted sets may still contain the post_id. At hydration time, batch-fetch returns null for that post_id and the feed service filters nulls before returning the response. No user-visible issue.


Code#

Fan-out Worker#

Java
public class FanoutWorker {
    private final RedisCluster redis;
    private final SocialGraphService graphService;
    private static final int CELEBRITY_THRESHOLD = 10_000;
    private static final int MAX_FEED_SIZE = 500;

    public void handlePostCreated(PostCreatedEvent event) {
        long authorFollowerCount = graphService.getFollowerCount(event.authorId());

        if (authorFollowerCount >= CELEBRITY_THRESHOLD) {
            // Celebrity path: store in shared sorted set, not per-follower.
            // Scored by timestamp so the read path can merge + paginate.
            String key = "celebrity:" + event.authorId();
            double score = event.createdAt().toEpochMilli();
            redis.zadd(key, score, event.postId());
            redis.zremrangeByRank(key, 0, -101); // keep last 100 posts
            return;
        }

        // Regular path: fan out to active followers only
        List<Long> followers = graphService.getActiveFollowers(
            event.authorId(),
            Duration.ofDays(7)
        );

        for (long followerId : followers) {
            String feedKey = "feed:" + followerId;
            double score = event.createdAt().toEpochMilli();

            redis.zadd(feedKey, score, event.postId());
            redis.zremrangeByRank(feedKey, 0, -(MAX_FEED_SIZE + 1));
        }
    }
}

Feed Read with Celebrity Merge and Cursor Pagination#

Java
public FeedResponse getFeed(long userId, String cursor, int limit) {
    // Exclusive upper bound so the cursor post is never returned twice.
    String maxScore = cursor != null ? "(" + cursor : "+inf";

    // 1. Pre-computed feed page (cursor-bounded, scored).
    Set<Tuple> regularPosts = redis.zrevrangeByScoreWithScores(
        "feed:" + userId, maxScore, "-inf", 0, limit
    );

    // 2. Celebrity posts, also cursor-bounded and scored, so they merge
    //    into the same timeline instead of jumping to the top on every page.
    List<String> followedCelebrities = graphService.getFollowedCelebrities(userId);
    List<Tuple> celebrityPosts = new ArrayList<>();
    for (String celebId : followedCelebrities) {
        celebrityPosts.addAll(redis.zrevrangeByScoreWithScores(
            "celebrity:" + celebId, maxScore, "-inf", 0, limit
        ));
    }

    // 3. Merge by score (timestamp) and take the top `limit` candidates.
    List<ScoredId> page = mergeByScore(regularPosts, celebrityPosts, limit);

    // 4. Cursor + has_more come from the CANDIDATES, before hydration, so a
    //    deleted post filtered out below can't corrupt pagination.
    String nextCursor = page.isEmpty() ? null
        : String.valueOf(page.get(page.size() - 1).score());
    boolean hasMore = page.size() == limit;

    // 5. Batch hydrate; drop nulls (deleted posts) for display only.
    List<Post> posts = postCache.mget(page.stream().map(ScoredId::id).toList())
        .stream().filter(Objects::nonNull).collect(toList());

    return new FeedResponse(posts, nextCursor, hasMore);
}

Trade-off Summary#

DecisionChosenAlternativeWhy
Fan-out strategyHybrid (write for regular, read for celebrities)Pure push or pure pullPure push collapses on celebrity accounts; pure pull collapses at read QPS
Celebrity threshold~10,000 followersNo distinctionAbove this, write amplification becomes unacceptable
Feed storageRedis sorted setDB feed tableSub-millisecond reads; O(log N) cursor pagination built in
PaginationCursor-based (timestamp)OFFSET/LIMITOFFSET scans and discards rows; breaks when new posts arrive mid-scroll
Post storage in feedPost IDs onlyFull post contentKeeps feed cache small; simplifies invalidation when content changes
Fan-out scopeActive followers only (last 7 days)All followers80-90% of followers are inactive; no point fanning out to them
Ranking approachTwo-pass (coarse at write, fine at read)Pure write-time or pure read-timePure write-time can't use fresh signals; pure read-time is too slow

Follow-up Questions#

Basic

Q: Why can't we just use OFFSET pagination?

When new posts arrive between page 1 and page 2, everything shifts. A post that was at position 20 is now at 21, so you either skip it or show it twice. OFFSET also requires the DB to scan and throw away all preceding rows on every request. Cursor uses the last-seen timestamp as an anchor so new posts don't affect older pages.

Q: Why store post IDs in the feed cache instead of full posts?

The same post appears in potentially millions of feeds. If like_count updates (which happens constantly), you'd need to update millions of cache entries. Storing only the ID means one source of truth for post content, updated in one place.

Q: What's the difference between the feed sorted set and the celebrity post list?

Feed sorted set is per-user, holds post IDs of people they follow (regular users only), scored by timestamp. Celebrity post list is per-celebrity, shared across all their followers, holds their last 100 posts. At read time both are merged.

Q: How does the fan-out service know who follows a celebrity?

It doesn't need to for the celebrity path. Celebrity posts skip fan-out entirely and are written to celebrity:{id}. The fan-out service only needs to look up followers for regular users, which is bounded by the threshold.

Senior follow-ups ↓

Q: A celebrity with 500M followers posts something viral. Walk me through exactly what happens and where the system could break.

Post Service writes to Post DB and publishes to Kafka. Fan-out Service consumes, checks follower count, sees it's above threshold, writes to celebrity:{id} cache and stops. The 500M follower list is never iterated. The risk is on the read side: 500M users may all open the app in a short window right after a major announcement. All their feed requests hit the celebrity post cache simultaneously. With a Redis distributed lock on cache rebuild, only one request refreshes from DB on cache miss. Others wait or serve without celebrity posts temporarily. Celebrity cache TTL should be low (1-5 minutes) with proactive refresh to prevent cold cache under load.

Q: How would you handle a user who deletes a post that's already in millions of feeds?

Mark as deleted in Post DB. Post cache is invalidated (or expires on TTL). At hydration time, batch-fetch returns null for that post_id and the feed service filters nulls before returning. Users who haven't loaded their feed yet will never see it. Users who already loaded it client-side would need a WebSocket or SSE push, or a client-side re-fetch, to remove it. Most platforms accept eventual consistency here since deletion propagates within seconds via TTL expiry.

Q: How would you redesign this if you had to guarantee the feed appears within 100ms even for cold users?

Pre-warm caches proactively. When a user logs in after inactivity, trigger a background job to rebuild their feed sorted set from the DB before they reach the feed endpoint. The login event goes to Kafka, a feed-rebuild worker picks it up and writes to Redis. By the time the user taps "Home", the feed is ready. For truly cold users (first time ever), fall back to a curated onboarding feed assembled from top posts by suggested accounts, cached once and served until they've built real follow history.

Q: Your fan-out queue has a 30-second lag during a spike. What do you do short-term and long-term?

Short-term: add Kafka consumers horizontally since they scale independently of producers. Increase partition count if consumers are maxed out. Notify users of staleness with a "new posts available" banner rather than auto-refreshing, which buys time without hurting UX.

Long-term: tune the celebrity threshold down so more accounts skip fan-out. Implement partial fan-out so you only push to active followers. Batch fan-out operations by processing 1000 followers per Kafka message instead of 1. Consider write-time scoring to reduce work at read time.


Minute-by-Minute Interview Playbook#

0 to 5 min: Requirements Ask the 6-7 questions above. Don't skip. The celebrity and scale questions directly determine your design. Confirm out-of-scope. Write FR/NFR on the board.

5 to 8 min: Capacity estimates Do the fan-out math out loud. The roughly 1M updates/sec number is what justifies the entire hybrid architecture. If you skip this, your design choices look arbitrary.

8 to 18 min: HLD Draw the two paths (write and read). Show Kafka between Post Service and Fan-out Service. This decouples post creation latency from fan-out latency. Show Redis as the primary feed read target.

18 to 38 min: Deep dives Lead with fan-out. Walk through push, explain why it breaks for celebrities, then pull, explain why it breaks at read scale, then hybrid. This single section is worth the most interview points. Then cover Redis sorted sets, cursor pagination, two-pass ranking. Move fast on things you know cold.

38 to 45 min: Failure scenarios and wrap-up Cover cold start and thundering herd if time allows. Summarize the trade-offs in one sentence: "We chose hybrid fan-out because neither pure approach scales alone here. Push collapses on write for celebrities, pull collapses on read for high-follow users."

Green flags

  • Immediately frames fan-out as the core tension
  • Proactively brings up celebrities before the interviewer asks
  • Correctly identifies the celebrity threshold as a tunable parameter, not a magic number
  • Explains cursor pagination by first describing why OFFSET breaks
  • Distinguishes feed cache (post IDs) from post cache (full content)

Red flags

  • Starts drawing boxes without asking about scale or follow topology
  • Describes pure push or pure pull without the hybrid
  • Uses OFFSET pagination
  • Stores full post content in the feed sorted set
  • Doesn't mention Kafka or any async decoupling between post creation and fan-out

Further Reading#

How Machine Learning Powers Facebook's News Feed Ranking
Meta's multi-pass scoring system, the signals they use, and how they rank content for 2 billion users in real time.
The Infrastructure Behind Twitter: Scale
How Twitter handles the fan-out problem, timeline caching with Haplo (their Redis fork), and the write path at scale.
System Design Interview Vol.1, Alex Xu, Chapter 11
The canonical interview reference for this problem. Good for a structured walkthrough before your interview.
Related: Design WhatsApp
Fan-out thinking transfers directly: message delivery at scale has the same push vs pull tradeoff.
Related: Design a Rate Limiter
Needed to protect the Post Service from write spam, a natural companion to this problem.

Try a different category