Core Entities and Fan-Out Strategies
Post: post_id, author_id, content, media_urls (array), created_at, like_count, comment_count, share_count. Follow: follower_id, followee_id, created_at. FeedItem: feed_id, user_id, post_id, score (ranking), inserted_at. (Materialized per-user feed.) Two fan-out strategies: Fan-out on write (push): when a user posts, immediately insert the post into the feed of all followers. Fast reads (pre-computed feed), slow writes (N followers = N inserts). Best for users with few followers. Fan-out on read (pull): when a user loads their feed, query the posts of all accounts they follow, sort by time. Slow reads, fast writes. Best for high-follower accounts (celebrities). Hybrid: fan-out on write for users with < 10K followers, fan-out on read (merge at read time) for celebrities. This is Instagram/Twitter's approach.
Feed Generation
class FeedService:
MAX_FEED_SIZE = 1000 # max items stored per user in Redis
def get_feed(self, user_id: int, page: int = 0,
page_size: int = 20) -> list[dict]:
# 1. Get pre-computed feed from Redis (sorted set by score)
start = page * page_size
end = start + page_size - 1
post_ids = self.redis.zrevrange(
f"feed:{user_id}", start, end, withscores=True
)
if len(post_ids) list:
# Pull model: join follows with posts
followees = self.db.query(
"SELECT followee_id FROM follows WHERE follower_id = %s", user_id
)
followee_ids = [f.followee_id for f in followees]
if not followee_ids: return []
posts = self.db.query(
"SELECT post_id FROM posts WHERE author_id = ANY(%s) "
"ORDER BY created_at DESC LIMIT %s OFFSET %s",
followee_ids, page_size, page * page_size
)
return [p.post_id for p in posts]
def fan_out_post(self, author_id: int, post_id: int,
score: float):
# Fan-out on write for users with small follower counts
followers = self.db.query(
"SELECT follower_id FROM follows WHERE followee_id = %s "
"AND follower_id NOT IN (SELECT id FROM users WHERE follower_count > 10000)",
author_id
)
pipe = self.redis.pipeline()
for f in followers:
pipe.zadd(f"feed:{f.follower_id}", {post_id: score})
pipe.zremrangebyrank( # cap feed size
f"feed:{f.follower_id}", 0, -(self.MAX_FEED_SIZE + 1)
)
pipe.execute()
Feed Ranking
Chronological sort (simplest): score = created_at timestamp. Sort descending. Engagement-based ranking: score = created_at + engagement_bonus. engagement_bonus = log(1 + likes) * W_like + log(1 + comments) * W_comment + log(1 + shares) * W_share. Weights tuned to platform goals. Personalized ranking: ML model scores each post for a user based on: author relationship strength (how often user interacts with this author), content affinity (topics user historically engages with), freshness decay (exponential decay by age), media type preference. For scale: pre-compute ranking scores when posts are created (fan-out writes the score into the sorted set). Re-rank on read for the top 50 posts if personalization signals need real-time data.
Feed Caching and Invalidation
Feed stored in Redis sorted set: key = feed:{user_id}, score = ranking_score, member = post_id. Max 1000 items per feed (ZREMRANGEBYRANK on each write). Cache individual posts: key = post:{post_id}, TTL 1h. Invalidation: post deletion requires iterating all follower feeds to remove the post – expensive for high-follower accounts. Mitigation: use a tombstone approach – mark post as deleted in DB; on read, filter out tombstoned posts from the feed without clearing the sorted set. Like count updates: increment a Redis counter on each like; batch-flush to DB every 60 seconds. This avoids a DB write per like under high engagement.
Meta’s News Feed is the canonical social feed system design topic. See common questions for Meta interview: social feed and News Feed system design.
Twitter system design interviews cover feed fan-out and ranking. Review patterns for Twitter/X interview: feed and timeline system design.
LinkedIn system design covers professional feed ranking and personalization. See patterns for LinkedIn interview: professional feed system design.