Social Media Feed
Social media platform with personalized feed generation, follow graph, and ranked content. Observer pattern for fan-out on post creation, Strategy pattern for swappable feed ranking (chronological vs engagement-weighted), and Iterator pattern for paginated feed traversal.
Key Abstractions
Facade coordinating users, posts, follows, and feed generation
Has profile, list of followers and following, and creates posts
Content with text, author, timestamp, likes count, and comments list
Text response to a post with author and timestamp
Aggregates and ranks posts from followed users, supports pagination
Strategy interface for ranking feed posts (Chronological, Engagement)
Manages follow/unfollow relationships and observer notifications
Class Diagram
The Key Insight
A social media feed looks trivial at first glance: users post content, followers see it. But the core engineering challenge is the feed assembly problem. When Alice follows 500 accounts, each posting multiple times a day, how do you build her personalized feed without melting your servers? The answer splits into two fundamental approaches. Fan-out-on-write precomputes every follower's feed when a post is created, which is fast to read but expensive to write, especially for accounts with millions of followers. Fan-out-on-read assembles the feed at query time by pulling recent posts from all followed accounts, which is cheap to write but expensive to read. Production systems like Twitter use a hybrid: fan-out-on-write for normal users, fan-out-on-read for celebrity accounts.
The second challenge is ranking. Chronological feeds are simple but surface stale content if a user hasn't checked the app in hours. Engagement-weighted feeds factor in likes, comments, and recency to surface the most interesting content first. The ranking algorithm is a product decision that changes frequently, so it must be decoupled from the feed assembly pipeline. Strategy pattern makes this clean: swap the ranking algorithm without touching the aggregation logic.
The third challenge is the follow graph itself. Following and unfollowing must be consistent, and post creation must notify followers without creating loops or redundant notifications. Observer pattern handles this: when a user posts, the system pushes updates to all followers. The follow manager maintains the graph and the notification dispatch independently from the content creation logic.
Requirements
Functional
- Users register and create text posts with timestamps
- Follow and unfollow other users
- View a personalized feed of posts from followed users
- Like and unlike posts, with deduplication (one like per user per post)
- Comment on posts
- Switch between feed ranking strategies (chronological, engagement-weighted)
- Paginate through the feed with configurable page size
Non-Functional
- Feed generation must not load all posts from all followed users into memory unbounded; use pagination
- Like deduplication must be enforced at the data layer, not through UI-only checks
- Feed ranking must be swappable at runtime without modifying the assembly pipeline
- Observer notifications must not create infinite loops on circular follow relationships
- Self-follows must be rejected
Design Decisions
Why fan-out-on-read instead of fan-out-on-write?
Fan-out-on-write precomputes each follower's feed when a post is created. For a user with 1 million followers, that means 1 million write operations per post. Celebrity accounts create massive write amplification. Fan-out-on-read computes the feed at query time by merging recent posts from followed accounts. It trades read-time computation for write simplicity. For this LLD, fan-out-on-read is the right choice because it keeps the data model simple and avoids precomputed feed storage. In production, hybrid approaches store precomputed feeds for normal users and compute on the fly for celebrity follows.
Why Strategy pattern for feed ranking?
Feed ranking is a product decision that evolves constantly. Instagram switched from chronological to engagement-based. Twitter added algorithmic timelines. Each change is an A/B test before a full rollout. If ranking is hardcoded into the feed assembly method, every experiment requires modifying core feed logic. Strategy pattern extracts ranking into its own class hierarchy. ChronologicalStrategy sorts by timestamp. EngagementStrategy weights likes, comments, and recency. Adding a "trending" strategy or a "close friends first" strategy means creating one new class that implements the rank method.
Why Observer pattern for post notifications?
When Bob creates a post, all of Bob's followers need to know about it. Polling is wasteful; followers would have to check Bob's profile repeatedly. Observer pattern inverts the control: Bob's post creation triggers a notification to all registered observers. The FollowManager maintains the follower graph and dispatches notifications. This decouples post creation from notification delivery. Adding new notification channels (push notifications, email digests, in-app badges) means registering new observers, not modifying the post creation logic.
Interview Follow-ups
- "How would you handle celebrity accounts with millions of followers?" Use a hybrid fan-out approach. For users with followers below a threshold (say 10,000), fan-out-on-write to precompute feeds. For celebrity accounts, use fan-out-on-read and merge their posts into followers' feeds at query time. Cache the celebrity's recent posts in a hot cache to minimize read latency.
- "How would you implement a 'Close Friends' feed?" Add a relationship tier to the follow graph. FollowManager stores a priority level per follow relationship. Introduce a CloseFriendsStrategy that filters posts to only those from users marked as close friends before ranking. The strategy composes with existing ranking strategies.
- "How would you add real-time feed updates?" WebSocket connections per active user session. When the Observer dispatches a notification, push a lightweight payload (post ID and preview) through the socket. The client prepends it to the feed without a full refresh. Use a message queue (Kafka, Redis Streams) between the observer and the WebSocket layer for reliability.
- "How would you prevent the feed from becoming stale for inactive users?" Set a maximum lookback window (e.g., 7 days). When assembling the feed, only gather posts within the window from followed users. For users returning after a long absence, show a "While you were away" summary with the top-engagement posts, computed lazily.
45-Minute LLD Playbook
Each phase is what the interviewer expects you to do and say. Concrete steps, not topic hints. Diagrams are what you would sketch on the board.
- 15 min
Clarify fan-out direction and feed scope
GoalPin down fan-out-on-read vs fan-out-on-write, ranking strategies in scope, and pagination contract. End by asking the diagram-or-code question.
Do & Say- ASK·1Open with the architectural fork: Fan-out-on-write precomputes each follower's feed at post time. Cheap reads, expensive writes for high-fanout accounts. Fan-out-on-read assembles at query time. Cheap writes, expensive reads. For this LLD I'll go fan-out-on-read and call out the hybrid for celebrities as a v2 note. OK to proceed?
- SAY·2Pin ranking scope: Two strategies in v1. Chronological sorts by created_at descending. Engagement uses (likes + 2 * comments) * recency_decay where decay is 1 / (1 + hours_old). New strategies are new classes, not new branches.
- SAY·3Lock the data model: Post has author, text, timestamp, a set of liker user_ids (set, not list, so likes dedupe at the data layer), and a comment list. Comment has author, text, timestamp. Like deduplication is enforced in Post.like, not in the UI.
- SAY·4Pin pagination contract: getFeed takes page and pageSize. Returns a Feed object that knows total_posts, total_pages, has_next_page. Default pageSize 20. No infinite scroll without cursor, but cursor is v2.
- SAY·5Park out of scope: self-follow rejection (enforced), circular notification loops (mention CRDTs for v2), real-time push via WebSocket (v2), celebrity special-casing (v2). Ask: Diagram first or jump to code starting from User and Post?
Interviewer is grading: You name fan-out-on-read vs fan-out-on-write in your opening sentence and pick one with a stated reason, not by default. You commit to set-based like dedup at the data layer instead of UI-only. You park celebrity fanout as v2 with a hybrid plan rather than ignoring it.
- 25-10 min
Sketch the pipeline and pattern map
GoalShow the four-stage pipeline (gather candidates, rank, paginate, return) and name where each pattern sits.
Do & Say- DRAW·1Draw the read path left to right: getFeed(user_id) -> get follower's following set from FollowManager -> gather posts from each followed user -> feedStrategy.rank(posts) -> Feed(ranked, page, pageSize). Say: Four stages, each cleanly separable.
- DRAW·2Draw the write path below: createPost -> Post(text, author, timestamp) -> FollowManager.notifyFollowers(author_id, post) -> each FeedObserver.onNewPost fires. Say: Observer fires at write time. Read path doesn't see it. They're independent.
- DRAW·3Sketch FeedStrategy as an interface with two concrete strategies. Note the EngagementStrategy weights: recency 1.0, likes 1.0, comments 2.0. Say: Comments weighted higher because they're a stronger engagement signal than a like.
- DRAW·4Sketch FollowManager holding three maps: followers (user_id -> set of follower_ids), following (user_id -> set of followee_ids), observers (user_id -> list of FeedObserver). Both follow maps are maintained on each follow/unfollow so reverse lookups are O(1).
- SAY·5Say: Two maps for one logical relationship. Write cost on follow is constant, read win is O(1) lookups in both directions.
- SAY·6Place the three patterns at clusters: Observer on FollowManager and FeedObserver, Strategy on FeedStrategy and its subclasses, Iterator on Feed pagination.
Interviewer is grading: You diagram the read path and write path as separate pipelines and name that separation. You volunteer the bidirectional followers/following maps with the O(1) lookup justification. You place each pattern at one specific cluster instead of generic labels.
- 325 min
Code in this sequence (bottom-up)
GoalType code in the order it appears in the reference. FeedObserver first, then User, Comment, Post, Feed, strategies, FollowManager, then FeedPlatform last. Talk while you code.
Do & Say- SAY·1Start with FeedObserver as an abstract class with on_new_post(post). Then User with user_id, username, posts list, add_post method. Then Comment with comment_id, text, author, created_at. Say: These are the pure entities. No behavior beyond holding data. (~3 min)
- SAY·2Code Post constructor: post_id, text, author, optional timestamp. Internally holds a _likes set (not list, for dedup) and a comments list. (~1 min)
- SAY·3Code Post methods: like(user_id) returns True if new, False if already in the set. unlike mirrors. add_comment appends. like_count returns len(_likes). engagement_score returns like_count + 2 * len(comments). (~2 min)
- SAY·4Say: Like dedup at the data layer. Set add returns False on duplicate. UI checks are not enough. (~1 min)
- SAY·5Code Feed constructor: takes all_posts, page, page_size. Computes start = page * page_size, end = start + page_size, slices into _posts. Properties for posts, page, total_posts, total_pages (math.ceil), has_next_page. (~2 min)
- SAY·6Say: Feed is the iterator over the ranked list. It owns pagination math so the platform never does the slice itself. (~1 min)
- SAY·7Code FeedStrategy abstract with rank(posts) -> list. Then ChronologicalStrategy: sorts by created_at descending. (~1 min)
- SAY·8Code EngagementStrategy: recency_weight, likes_weight, comments_weight in constructor. rank computes score = (like_count * likes_weight + comments * comments_weight) * recency_decay where recency_decay = 1 / (1 + hours_old * recency_weight). (~2 min)
- SAY·9Say: recency_decay handles the cold-start problem. A post 100 hours old barely scores no matter how many likes. Intentional. (~1 min)
- SAY·10Code FollowManager fields: _followers (followee_id -> set), _following (follower_id -> set), _observers (user_id -> list). follow validates non-self, updates both maps. unfollow discards from both. register_observer appends. (~2 min)
- SAY·11Code notify_followers: iterate followers of the author, for each follower iterate their observers and call on_new_post. (~1 min)
- SAY·12Say: Both directions on every follow trades two writes for two O(1) reads. For our read-heavy workload that's the right call. (~1 min)
- SAY·13Code LoggingObserver: concrete FeedObserver that prints and stores notifications. (~1 min)
- SAY·14Code FeedPlatform fields: _users, _posts, _follow_manager, _feed_strategy (default ChronologicalStrategy). Implement register_user and create_post (creates Post, adds to author's posts, calls notify_followers). (~1 min)
- SAY·15Add the write helpers: like_post (delegates to post.like, raises on duplicate), comment_on_post, follow/unfollow (delegate to FollowManager), set_feed_strategy, register_observer. (~1 min)
- SAY·16Code the load-bearing get_feed: get following set, gather posts from each user, call strategy.rank, return Feed(ranked, page, page_size). (~1 min)
- SAY·17Say: create_post is the only write path that triggers notifications. Reads never trigger writes. That asymmetry keeps the system simple. (~1 min)
- SAY·18Walk-through, follow path: Alice follows Bob. Bob posts p1. notify_followers fires, Alice's LoggingObserver records. Alice calls getFeed(her_id, page=0, pageSize=10). Following = {bob}, gather Bob's posts, rank chronologically, page 0 returns up to 10. (~1 min)
- SAY·19Walk-through, unfollow path: Alice unfollows Bob. getFeed returns 0 posts. Observer still has the historical notification. Self-test passes.
Interviewer is grading: Code compiles in order. You name set-based like dedup explicitly. You commit to bidirectional follow maps with the O(1) read justification. You name the createPost asymmetry (writes trigger notifications, reads don't). Mental walk-through with concrete users before declaring done.
- 45 min
Trade-offs and wrap-up
GoalName two trade-offs, volunteer one extension, close with a one-sentence summary.
Do & Say- SAY·1Trade-off one, fan-out-on-read over fan-out-on-write: A celebrity with 10M followers means 10M writes per post under fan-out-on-write. Under fan-out-on-read, reads pull recent posts on demand. I picked read because the data model is simpler.
- SAY·2Defend the hybrid path: Production hybrids do write for normal accounts and read for celebrities. The Strategy abstraction means the assembly pipeline doesn't change.
- SAY·3Trade-off two, Strategy over a sort-mode boolean: A boolean works for two modes. Adding Close Friends, Trending, and For You forces an if/elif cascade in the assembly method. Strategy keeps each ranking algorithm in its own class with its own knobs. New experiment is one new class.
- SAY·4Extension to volunteer, hybrid fan-out for celebrities: Threshold-based dispatch. Users with followers below 10K get fan-out-on-write to a precomputed feed table. Above 10K, fan-out-on-read with hot caching of recent posts. The feed assembly path merges both: precomputed feed plus on-demand celebrity posts.
- WATCH·5Be ready for real-time updates: WebSocket per active session. FeedObserver dispatches a lightweight payload (post_id, preview) over the socket. Client prepends to the feed without a full refresh. Kafka or Redis Streams between the observer and the WebSocket layer for delivery reliability.
- SAY·6Close with one sentence: Observer on FollowManager for decoupled fan-out. Strategy on FeedStrategy for swappable ranking. Iterator on Feed for bounded memory pagination. And set-based like dedup at the data layer. Three patterns, two pipelines, one platform.
Interviewer is grading: Trade-offs are framed as 'we accept X cost because Y benefit'. The hybrid extension is concrete (mentions a follower threshold and a precomputed feed table). The real-time answer separates the dispatch path from the delivery layer (Kafka between observer and WebSocket).
Code Implementation
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import Optional
import uuid
import math
# --- Feed Observer Interface ---
class FeedObserver(ABC):
@abstractmethod
def on_new_post(self, post: "Post") -> None: ...
# --- User ---
class User:
"""A platform user who can create posts, follow others, and interact."""
def __init__(self, user_id: str, username: str):
self.user_id = user_id
self.username = username
self.posts: list["Post"] = []
def add_post(self, post: "Post") -> None:
self.posts.append(post)
def __repr__(self) -> str:
return f"User({self.username})"
# --- Comment ---
class Comment:
def __init__(self, comment_id: str, text: str, author: User):
self.comment_id = comment_id
self.text = text
self.author = author
self.created_at = datetime.now()
def __repr__(self) -> str:
return f"Comment(by={self.author.username}, '{self.text[:30]}')"
# --- Post ---
class Post:
"""A piece of content with likes and comments.
Likes are tracked as a set of user IDs to prevent duplicates."""
def __init__(self, post_id: str, text: str, author: User,
timestamp: Optional[datetime] = None):
self.post_id = post_id
self.text = text
self.author = author
self.created_at = timestamp or datetime.now()
self._likes: set[str] = set()
self.comments: list[Comment] = []
def like(self, user_id: str) -> bool:
"""Returns True if the like was new, False if already liked."""
if user_id in self._likes:
return False
self._likes.add(user_id)
return True
def unlike(self, user_id: str) -> bool:
"""Returns True if removed, False if wasn't liked."""
if user_id not in self._likes:
return False
self._likes.discard(user_id)
return True
def add_comment(self, comment: Comment) -> None:
self.comments.append(comment)
@property
def like_count(self) -> int:
return len(self._likes)
def is_liked_by(self, user_id: str) -> bool:
return user_id in self._likes
@property
def engagement_score(self) -> float:
"""Raw engagement: likes + 2*comments (comments are higher signal)."""
return self.like_count + 2 * len(self.comments)
def __repr__(self) -> str:
return (f"Post('{self.text[:40]}', by={self.author.username}, "
f"likes={self.like_count}, comments={len(self.comments)})")
# --- Feed (with pagination / iterator) ---
class Feed:
"""A paginated view over a ranked list of posts."""
def __init__(self, posts: list[Post], page: int, page_size: int):
self._all_posts = posts
self._page = page
self._page_size = page_size
start = page * page_size
end = start + page_size
self._posts = posts[start:end]
@property
def posts(self) -> list[Post]:
return self._posts
@property
def page(self) -> int:
return self._page
@property
def total_posts(self) -> int:
return len(self._all_posts)
@property
def total_pages(self) -> int:
if self._page_size == 0:
return 0
return math.ceil(len(self._all_posts) / self._page_size)
def has_next_page(self) -> bool:
return self._page < self.total_pages - 1
def __repr__(self) -> str:
return (f"Feed(page={self._page + 1}/{self.total_pages}, "
f"showing={len(self._posts)}, total={self.total_posts})")
# --- Feed Strategies ---
class FeedStrategy(ABC):
@abstractmethod
def rank(self, posts: list[Post]) -> list[Post]: ...
class ChronologicalStrategy(FeedStrategy):
"""Most recent posts first."""
def rank(self, posts: list[Post]) -> list[Post]:
return sorted(posts, key=lambda p: p.created_at, reverse=True)
class EngagementStrategy(FeedStrategy):
"""Ranks by a weighted combination of engagement and recency.
Score = (likes + 2*comments) * recency_decay
where recency_decay = 1 / (1 + hours_since_post)."""
def __init__(self, recency_weight: float = 1.0,
likes_weight: float = 1.0,
comments_weight: float = 2.0):
self.recency_weight = recency_weight
self.likes_weight = likes_weight
self.comments_weight = comments_weight
def rank(self, posts: list[Post]) -> list[Post]:
now = datetime.now()
def score(post: Post) -> float:
hours_old = max(0.1, (now - post.created_at).total_seconds() / 3600)
recency_decay = 1.0 / (1.0 + hours_old * self.recency_weight)
engagement = (post.like_count * self.likes_weight
+ len(post.comments) * self.comments_weight)
return engagement * recency_decay
return sorted(posts, key=score, reverse=True)
# --- Follow Manager (Observer pattern) ---
class FollowManager:
"""Manages the follow graph and notifies observers when posts are created."""
def __init__(self):
self._followers: dict[str, set[str]] = {} # user_id -> set of follower IDs
self._following: dict[str, set[str]] = {} # user_id -> set of followee IDs
self._observers: dict[str, list[FeedObserver]] = {}
def follow(self, follower_id: str, followee_id: str) -> None:
if follower_id == followee_id:
raise ValueError("Users cannot follow themselves")
self._following.setdefault(follower_id, set()).add(followee_id)
self._followers.setdefault(followee_id, set()).add(follower_id)
def unfollow(self, follower_id: str, followee_id: str) -> None:
self._following.get(follower_id, set()).discard(followee_id)
self._followers.get(followee_id, set()).discard(follower_id)
def get_following(self, user_id: str) -> set[str]:
return self._following.get(user_id, set()).copy()
def get_followers(self, user_id: str) -> set[str]:
return self._followers.get(user_id, set()).copy()
def register_observer(self, user_id: str, observer: FeedObserver) -> None:
self._observers.setdefault(user_id, []).append(observer)
def notify_followers(self, author_id: str, post: "Post") -> None:
"""Notifies all observers registered to followers of the author."""
for follower_id in self._followers.get(author_id, set()):
for observer in self._observers.get(follower_id, []):
observer.on_new_post(post)
# --- Simple notification observer for demo ---
class LoggingObserver(FeedObserver):
def __init__(self, username: str):
self.username = username
self.notifications: list[str] = []
def on_new_post(self, post: "Post") -> None:
msg = f"{self.username} notified: new post by {post.author.username}"
self.notifications.append(msg)
print(f" [Observer] {msg}")
# --- Feed Platform Facade ---
class FeedPlatform:
"""Central coordinator. All interactions go through here.
Uses fan-out-on-read: feed is assembled at query time from followed users' posts."""
def __init__(self):
self._users: dict[str, User] = {}
self._posts: dict[str, Post] = {}
self._follow_manager = FollowManager()
self._feed_strategy: FeedStrategy = ChronologicalStrategy()
self._like_log: dict[str, set[str]] = {} # post_id -> set of user_ids
def register_user(self, username: str) -> User:
user_id = str(uuid.uuid4())[:8]
user = User(user_id, username)
self._users[user_id] = user
return user
def create_post(self, user_id: str, text: str,
timestamp: Optional[datetime] = None) -> Post:
author = self._users[user_id]
post_id = str(uuid.uuid4())[:8]
post = Post(post_id, text, author, timestamp)
self._posts[post_id] = post
author.add_post(post)
# Observer: notify followers
self._follow_manager.notify_followers(user_id, post)
return post
def like_post(self, user_id: str, post_id: str) -> None:
if user_id not in self._users:
raise ValueError("User not found")
post = self._posts[post_id]
if not post.like(user_id):
raise ValueError(f"User already liked this post")
def unlike_post(self, user_id: str, post_id: str) -> None:
post = self._posts[post_id]
if not post.unlike(user_id):
raise ValueError(f"User hasn't liked this post")
def comment_on_post(self, user_id: str, post_id: str, text: str) -> Comment:
author = self._users[user_id]
post = self._posts[post_id]
comment_id = str(uuid.uuid4())[:8]
comment = Comment(comment_id, text, author)
post.add_comment(comment)
return comment
def follow(self, follower_id: str, followee_id: str) -> None:
self._follow_manager.follow(follower_id, followee_id)
def unfollow(self, follower_id: str, followee_id: str) -> None:
self._follow_manager.unfollow(follower_id, followee_id)
def register_observer(self, user_id: str, observer: FeedObserver) -> None:
self._follow_manager.register_observer(user_id, observer)
def set_feed_strategy(self, strategy: FeedStrategy) -> None:
self._feed_strategy = strategy
def get_feed(self, user_id: str, page: int = 0, page_size: int = 20) -> Feed:
"""Fan-out-on-read: collect posts from all followed users, rank, paginate."""
following_ids = self._follow_manager.get_following(user_id)
# Gather all posts from followed users
candidate_posts: list[Post] = []
for uid in following_ids:
user = self._users.get(uid)
if user:
candidate_posts.extend(user.posts)
# Rank using current strategy
ranked = self._feed_strategy.rank(candidate_posts)
return Feed(ranked, page, page_size)
if __name__ == "__main__":
platform = FeedPlatform()
# Register users
alice = platform.register_user("alice")
bob = platform.register_user("bob")
charlie = platform.register_user("charlie")
# Set up observer for Alice
alice_observer = LoggingObserver("alice")
platform.register_observer(alice.user_id, alice_observer)
# Alice follows Bob and Charlie
platform.follow(alice.user_id, bob.user_id)
platform.follow(alice.user_id, charlie.user_id)
print(f"Alice follows Bob and Charlie")
# Bob and Charlie create posts with staggered timestamps
now = datetime.now()
p1 = platform.create_post(bob.user_id,
"Just shipped a new feature! Really proud of the team.",
timestamp=now - timedelta(hours=3))
p2 = platform.create_post(charlie.user_id,
"Great article on system design patterns. Must read for interviews.",
timestamp=now - timedelta(hours=2))
p3 = platform.create_post(bob.user_id,
"Anyone else excited about the new Python release?",
timestamp=now - timedelta(hours=1))
p4 = platform.create_post(charlie.user_id,
"Coffee and code. The perfect morning routine.",
timestamp=now - timedelta(minutes=30))
# --- Chronological feed ---
print("\n--- Alice's feed (Chronological) ---")
platform.set_feed_strategy(ChronologicalStrategy())
feed = platform.get_feed(alice.user_id, page=0, page_size=10)
print(feed)
for i, post in enumerate(feed.posts):
print(f" {i + 1}. {post}")
# --- Add engagement: likes and comments ---
print("\n--- Adding engagement ---")
platform.like_post(alice.user_id, p1.post_id)
platform.like_post(charlie.user_id, p1.post_id)
platform.like_post(alice.user_id, p2.post_id)
platform.comment_on_post(alice.user_id, p1.post_id,
"Congrats! What was the hardest part?")
platform.comment_on_post(bob.user_id, p1.post_id,
"Thanks! The database migration was tricky.")
platform.comment_on_post(alice.user_id, p2.post_id,
"Bookmarked this. Great share!")
print(f" p1 (Bob's feature post): {p1.like_count} likes, "
f"{len(p1.comments)} comments")
print(f" p2 (Charlie's article): {p2.like_count} likes, "
f"{len(p2.comments)} comments")
print(f" p3 (Bob's Python post): {p3.like_count} likes, "
f"{len(p3.comments)} comments")
print(f" p4 (Charlie's coffee post): {p4.like_count} likes, "
f"{len(p4.comments)} comments")
# --- Engagement-based feed ---
print("\n--- Alice's feed (Engagement-weighted) ---")
platform.set_feed_strategy(EngagementStrategy())
feed = platform.get_feed(alice.user_id, page=0, page_size=10)
print(feed)
for i, post in enumerate(feed.posts):
print(f" {i + 1}. {post}")
# --- Pagination demo ---
print("\n--- Pagination (page_size=2) ---")
platform.set_feed_strategy(ChronologicalStrategy())
page_num = 0
while True:
feed = platform.get_feed(alice.user_id, page=page_num, page_size=2)
print(f" Page {feed.page + 1}/{feed.total_pages}:")
for post in feed.posts:
print(f" - {post}")
if not feed.has_next_page():
break
page_num += 1
# --- Like deduplication ---
print("\n--- Like deduplication ---")
try:
platform.like_post(alice.user_id, p1.post_id)
print("ERROR: should have raised")
except ValueError as e:
print(f" Expected error: {e}")
# --- Unfollow ---
print("\n--- Unfollow Bob ---")
platform.unfollow(alice.user_id, bob.user_id)
feed = platform.get_feed(alice.user_id, page=0, page_size=10)
print(f" Alice's feed after unfollowing Bob: {len(feed.posts)} posts")
for post in feed.posts:
print(f" - {post}")
# --- Observer notifications ---
print(f"\n--- Observer notifications ({len(alice_observer.notifications)}) ---")
for n in alice_observer.notifications:
print(f" {n}")
# Assertions
assert p1.like_count == 2, f"Expected 2 likes, got {p1.like_count}"
assert len(p1.comments) == 2, f"Expected 2 comments, got {len(p1.comments)}"
assert len(feed.posts) == 2, "After unfollowing Bob, only Charlie's posts"
assert feed.posts[0].author.username == "charlie"
assert len(alice_observer.notifications) == 4, "4 posts created while following"
print("\nAll assertions passed.")Interview Grading by Level
What an interviewer at each level expects to see in your answer. Use this to calibrate, not to perform.
Junior Engineer (L3)
Lists the three patterns and writes Post, Feed, and a working chronological ranking, but fan-out direction and like dedup stay shaky.
- Names Observer, Strategy, and Iterator correctly.
- Writes Post with likes and comments collections.
- Implements at least ChronologicalStrategy as a sort by timestamp.
- Recognizes that getFeed needs pagination, even if implemented as an in-memory slice.
- Adds a FollowManager with separate followers and following maps.
- Does not name fan-out-on-read vs fan-out-on-write or picks one arbitrarily.
- Stores likes as a list instead of a set, so duplicates accumulate.
- Hardcodes the chronological sort inside getFeed instead of behind a Strategy.
- Maintains only the following map and not the followers map, forcing O(n) reverse lookups.
- Lets a user follow themselves, no self-follow validation.
Mid-Level Engineer (L4)
Drives the design with clear fan-out justification, set-based like dedup, and a clean Strategy split between Chronological and Engagement.
- Picks fan-out-on-read with a stated reason and notes the celebrity caveat.
- Implements likes as a set on Post with like returning a bool on dedup.
- Writes EngagementStrategy with recency decay (1 / (1 + hours_old)) and explains the cold-start motivation.
- Maintains followers and following as separate maps and justifies the bidirectional cost.
- Treats FollowManager as the only place that knows about the follower graph.
- Does not volunteer hybrid fan-out for celebrities or WebSocket-based real-time as the next extensions.
- Misses comments-weighted-higher-than-likes as an intentional product signal.
- Treats Feed pagination as a list slice without an interface for cursor-based v2.
Senior Engineer (L5+)
Volunteers hybrid fan-out and real-time delivery before being asked, names the data-layer dedup invariant, and frames each pattern as a solution to a specific scale or coupling problem.
- Volunteers hybrid fan-out (write for normal, read for celebrities) with a follower-count threshold before being asked.
- Names set-based like dedup at the data layer as the invariant that prevents trivial vote manipulation, framed as 'UI checks are not enough'.
- Frames each pattern around the failure it prevents: Observer for poll-free notifications, Strategy for ranking experimentation without touching assembly, Iterator for bounded memory at scale.
- Proposes WebSocket dispatch with Kafka or Redis Streams as the durability layer between the observer and the socket, separating dispatch from delivery.
- Defends bidirectional follower/following maps by naming the concrete query that O(1) reverse lookup serves: notify_followers is hot path, must not scan.
- Names comments weighted higher than likes (factor of 2) as an intentional product decision, not an arbitrary number.
- Closes with a one-sentence summary that names all three patterns, the two pipelines, and the data-layer dedup invariant in under 20 seconds.
Common Mistakes
- ✗Loading all posts from all followed users into memory at once. With 500 follows each posting daily, that's thousands of posts to sort. Use pagination and limit the merge window.
- ✗Not separating feed ranking from feed assembly. Hardcoding chronological sort means every new ranking experiment requires editing the feed generation code.
- ✗Letting users like/comment without tracking who did it. Without deduplication, a user can like the same post infinite times.
- ✗Circular follow relationships causing infinite notification loops. The observer must not trigger re-notification when updating derived state.
Key Points
- ✓Fan-out-on-read vs fan-out-on-write is THE key architectural decision. Fan-out-on-read computes the feed at query time by merging posts from all followed users. Fan-out-on-write precomputes feeds when a post is created. This LLD uses fan-out-on-read for simplicity, but production systems use a hybrid.
- ✓Strategy pattern makes feed ranking testable and swappable. Chronological is simple (sort by timestamp). Engagement-weighted factors in likes, comments, and recency. Adding a new ranking algorithm means one new class.
- ✓Observer pattern decouples post creation from feed updates and notifications. When a user posts, followers don't need to poll. The system pushes updates.
- ✓Pagination via iterator prevents loading entire feed into memory. Real feeds have thousands of posts. Load 20 at a time with a cursor.