# Low-Level Design: Social Media Feed (Follow, Post, Fan-out, Likes)

Source: https://www.techinterview.org/post/3233464486/lld-social-media-feed/
Updated: 2026-04-17 · techinterview.org

## Low-Level Design: Social Media Feed (Twitter/Instagram)

A social media feed system manages posts, follows, likes, and generates a personalized home timeline. It is a common LLD question at Meta, Twitter, LinkedIn, and Snap.

## Core Entities


```
from dataclasses import dataclass, field
from enum import Enum
from datetime import datetime
from typing import Optional
import uuid

class PostType(Enum):
    TEXT = "text"
    IMAGE = "image"
    VIDEO = "video"
    REPOST = "repost"

@dataclass
class User:
    user_id: str
    username: str
    display_name: str
    bio: str = ""
    follower_count: int = 0
    following_count: int = 0
    is_verified: bool = False

@dataclass
class Post:
    post_id: str
    author_id: str
    content: str
    post_type: PostType = PostType.TEXT
    media_urls: list[str] = field(default_factory=list)
    like_count: int = 0
    repost_count: int = 0
    reply_count: int = 0
    parent_post_id: Optional[str] = None  # for replies
    created_at: datetime = field(default_factory=datetime.utcnow)
    is_deleted: bool = False

@dataclass
class Follow:
    follower_id: str
    followee_id: str
    created_at: datetime = field(default_factory=datetime.utcnow)
```


## Follow/Unfollow Service


```
class FollowService:
    def __init__(self):
        # followee_id -> set of follower_ids
        self._followers: dict[str, set] = {}
        # follower_id -> set of followee_ids
        self._following: dict[str, set] = {}
        self._users: dict[str, User] = {}

    def follow(self, follower_id: str, followee_id: str) -> None:
        if follower_id == followee_id:
            raise ValueError("Cannot follow yourself")
        if followee_id in self._following.get(follower_id, set()):
            return  # already following
        self._following.setdefault(follower_id, set()).add(followee_id)
        self._followers.setdefault(followee_id, set()).add(follower_id)
        # Update counts
        if follower_id in self._users:
            self._users[follower_id].following_count += 1
        if followee_id in self._users:
            self._users[followee_id].follower_count += 1

    def unfollow(self, follower_id: str, followee_id: str) -> None:
        if followee_id not in self._following.get(follower_id, set()):
            return  # not following
        self._following[follower_id].discard(followee_id)
        self._followers[followee_id].discard(follower_id)
        if follower_id in self._users:
            self._users[follower_id].following_count -= 1
        if followee_id in self._users:
            self._users[followee_id].follower_count -= 1

    def get_followers(self, user_id: str) -> set[str]:
        return self._followers.get(user_id, set())

    def get_following(self, user_id: str) -> set[str]:
        return self._following.get(user_id, set())

    def is_following(self, follower_id: str, followee_id: str) -> bool:
        return followee_id in self._following.get(follower_id, set())
```


## Feed Service — Fan-out on Write (Push Model)


```
import heapq

class FeedService:
    """
    Push model: on post creation, write to all followers' feeds immediately.
    Pros: fast reads (O(1) feed lookup).
    Cons: expensive writes for high-follower accounts (celebrities with 10M+ followers).
    Solution: hybrid — push for regular users, pull for celebrities.
    """
    CELEBRITY_THRESHOLD = 1_000_000  # followers

    def __init__(self, follow_svc, post_store):
        self.follow_svc = follow_svc
        self.posts = post_store
        # user_id -> list of (timestamp, post_id) for their home feed
        self._feeds: dict[str, list] = {}

    def publish_post(self, post: Post, user: User) -> None:
        """Fan-out post to all followers' feeds (push model)."""
        followers = self.follow_svc.get_followers(post.author_id)
        entry = (-post.created_at.timestamp(), post.post_id)
        # Also add to author's own feed
        heapq.heappush(self._feeds.setdefault(post.author_id, []), entry)
        if user.follower_count >= self.CELEBRITY_THRESHOLD:
            return  # skip fan-out for celebrities; use pull on read
        for follower_id in followers:
            heapq.heappush(self._feeds.setdefault(follower_id, []), entry)

    def get_feed(self, user_id: str, limit: int = 20) -> list[str]:
        """Return top-limit post IDs for user's home feed."""
        heap = self._feeds.get(user_id, [])
        # Pull celebrity posts (not pre-pushed)
        following = self.follow_svc.get_following(user_id)
        celebrity_posts = []
        for followee_id in following:
            followee = self.posts.get_user(followee_id)
            if followee and followee.follower_count >= self.CELEBRITY_THRESHOLD:
                recent = self.posts.get_recent_posts(followee_id, limit=5)
                for p in recent:
                    celebrity_posts.append((-p.created_at.timestamp(), p.post_id))

        # Merge heap + celebrity posts
        all_posts = list(heap) + celebrity_posts
        all_posts.sort()
        seen = set()
        result = []
        for _, post_id in all_posts:
            if post_id not in seen and len(result) < limit:
                seen.add(post_id)
                result.append(post_id)
        return result
```


## Like Service with Idempotency


```
class LikeService:
    def __init__(self, post_store):
        self.posts = post_store
        self._likes: dict[str, set] = {}  # post_id -> set of user_ids

    def like(self, user_id: str, post_id: str) -> bool:
        liked_by = self._likes.setdefault(post_id, set())
        if user_id in liked_by:
            return False  # already liked, idempotent
        liked_by.add(user_id)
        post = self.posts.get(post_id)
        if post:
            post.like_count += 1
        return True

    def unlike(self, user_id: str, post_id: str) -> bool:
        liked_by = self._likes.get(post_id, set())
        if user_id not in liked_by:
            return False  # not liked
        liked_by.discard(user_id)
        post = self.posts.get(post_id)
        if post:
            post.like_count = max(0, post.like_count - 1)
        return True

    def is_liked(self, user_id: str, post_id: str) -> bool:
        return user_id in self._likes.get(post_id, set())

    def like_count(self, post_id: str) -> int:
        return len(self._likes.get(post_id, set()))
```


## Design Trade-offs

| Decision | Choice | Trade-off |
| --- | --- | --- |
| Feed generation | Hybrid push/pull | Fast reads for regular users; celebrities use pull to avoid 10M fan-out writes |
| Like storage | Set per post | O(1) toggle and lookup; in production use Redis SET or DB with unique constraint |
| Follow graph | In-memory sets | For LLD; production uses Graph DB (Neo4j) or adjacency list in Cassandra |
| Feed ordering | Reverse chronological (min-heap) | Simple; algorithmic ranking adds engagement signals |

Asked at: [Snap Interview Guide](https://techinterview.org/snap-interview/)

Asked at: [Twitter/X Interview Guide](https://techinterview.org/twitter-interview/)

Asked at: [LinkedIn Interview Guide](https://techinterview.org/linkedin-interview/)

Asked at: [Airbnb Interview Guide](https://techinterview.org/airbnb-interview/)
