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.
Code Implementation
1 from abc import ABC, abstractmethod
2 from datetime import datetime, timedelta
3 from typing import Optional
4 import uuid
5 import math
6
7
8 # --- Feed Observer Interface ---
9
10 class FeedObserver(ABC):
11 @abstractmethod
12 def on_new_post(self, post: "Post") -> None: ...
13
14
15 # --- User ---
16
17 class User:
18 """A platform user who can create posts, follow others, and interact."""
19
20 def __init__(self, user_id: str, username: str):
21 self.user_id = user_id
22 self.username = username
23 self.posts: list["Post"] = []
24
25 def add_post(self, post: "Post") -> None:
26 self.posts.append(post)
27
28 def __repr__(self) -> str:
29 return f"User({self.username})"
30
31
32 # --- Comment ---
33
34 class Comment:
35 def __init__(self, comment_id: str, text: str, author: User):
36 self.comment_id = comment_id
37 self.text = text
38 self.author = author
39 self.created_at = datetime.now()
40
41 def __repr__(self) -> str:
42 return f"Comment(by={self.author.username}, '{self.text[:30]}')"
43
44
45 # --- Post ---
46
47 class Post:
48 """A piece of content with likes and comments.
49 Likes are tracked as a set of user IDs to prevent duplicates."""
50
51 def __init__(self, post_id: str, text: str, author: User,
52 timestamp: Optional[datetime] = None):
53 self.post_id = post_id
54 self.text = text
55 self.author = author
56 self.created_at = timestamp or datetime.now()
57 self._likes: set[str] = set()
58 self.comments: list[Comment] = []
59
60 def like(self, user_id: str) -> bool:
61 """Returns True if the like was new, False if already liked."""
62 if user_id in self._likes:
63 return False
64 self._likes.add(user_id)
65 return True
66
67 def unlike(self, user_id: str) -> bool:
68 """Returns True if removed, False if wasn't liked."""
69 if user_id not in self._likes:
70 return False
71 self._likes.discard(user_id)
72 return True
73
74 def add_comment(self, comment: Comment) -> None:
75 self.comments.append(comment)
76
77 @property
78 def like_count(self) -> int:
79 return len(self._likes)
80
81 def is_liked_by(self, user_id: str) -> bool:
82 return user_id in self._likes
83
84 @property
85 def engagement_score(self) -> float:
86 """Raw engagement: likes + 2*comments (comments are higher signal)."""
87 return self.like_count + 2 * len(self.comments)
88
89 def __repr__(self) -> str:
90 return (f"Post('{self.text[:40]}', by={self.author.username}, "
91 f"likes={self.like_count}, comments={len(self.comments)})")
92
93
94 # --- Feed (with pagination / iterator) ---
95
96 class Feed:
97 """A paginated view over a ranked list of posts."""
98
99 def __init__(self, posts: list[Post], page: int, page_size: int):
100 self._all_posts = posts
101 self._page = page
102 self._page_size = page_size
103 start = page * page_size
104 end = start + page_size
105 self._posts = posts[start:end]
106
107 @property
108 def posts(self) -> list[Post]:
109 return self._posts
110
111 @property
112 def page(self) -> int:
113 return self._page
114
115 @property
116 def total_posts(self) -> int:
117 return len(self._all_posts)
118
119 @property
120 def total_pages(self) -> int:
121 if self._page_size == 0:
122 return 0
123 return math.ceil(len(self._all_posts) / self._page_size)
124
125 def has_next_page(self) -> bool:
126 return self._page < self.total_pages - 1
127
128 def __repr__(self) -> str:
129 return (f"Feed(page={self._page + 1}/{self.total_pages}, "
130 f"showing={len(self._posts)}, total={self.total_posts})")
131
132
133 # --- Feed Strategies ---
134
135 class FeedStrategy(ABC):
136 @abstractmethod
137 def rank(self, posts: list[Post]) -> list[Post]: ...
138
139
140 class ChronologicalStrategy(FeedStrategy):
141 """Most recent posts first."""
142
143 def rank(self, posts: list[Post]) -> list[Post]:
144 return sorted(posts, key=lambda p: p.created_at, reverse=True)
145
146
147 class EngagementStrategy(FeedStrategy):
148 """Ranks by a weighted combination of engagement and recency.
149 Score = (likes + 2*comments) * recency_decay
150 where recency_decay = 1 / (1 + hours_since_post)."""
151
152 def __init__(self, recency_weight: float = 1.0,
153 likes_weight: float = 1.0,
154 comments_weight: float = 2.0):
155 self.recency_weight = recency_weight
156 self.likes_weight = likes_weight
157 self.comments_weight = comments_weight
158
159 def rank(self, posts: list[Post]) -> list[Post]:
160 now = datetime.now()
161
162 def score(post: Post) -> float:
163 hours_old = max(0.1, (now - post.created_at).total_seconds() / 3600)
164 recency_decay = 1.0 / (1.0 + hours_old * self.recency_weight)
165 engagement = (post.like_count * self.likes_weight
166 + len(post.comments) * self.comments_weight)
167 return engagement * recency_decay
168
169 return sorted(posts, key=score, reverse=True)
170
171
172 # --- Follow Manager (Observer pattern) ---
173
174 class FollowManager:
175 """Manages the follow graph and notifies observers when posts are created."""
176
177 def __init__(self):
178 self._followers: dict[str, set[str]] = {} # user_id -> set of follower IDs
179 self._following: dict[str, set[str]] = {} # user_id -> set of followee IDs
180 self._observers: dict[str, list[FeedObserver]] = {}
181
182 def follow(self, follower_id: str, followee_id: str) -> None:
183 if follower_id == followee_id:
184 raise ValueError("Users cannot follow themselves")
185 self._following.setdefault(follower_id, set()).add(followee_id)
186 self._followers.setdefault(followee_id, set()).add(follower_id)
187
188 def unfollow(self, follower_id: str, followee_id: str) -> None:
189 self._following.get(follower_id, set()).discard(followee_id)
190 self._followers.get(followee_id, set()).discard(follower_id)
191
192 def get_following(self, user_id: str) -> set[str]:
193 return self._following.get(user_id, set()).copy()
194
195 def get_followers(self, user_id: str) -> set[str]:
196 return self._followers.get(user_id, set()).copy()
197
198 def register_observer(self, user_id: str, observer: FeedObserver) -> None:
199 self._observers.setdefault(user_id, []).append(observer)
200
201 def notify_followers(self, author_id: str, post: "Post") -> None:
202 """Notifies all observers registered to followers of the author."""
203 for follower_id in self._followers.get(author_id, set()):
204 for observer in self._observers.get(follower_id, []):
205 observer.on_new_post(post)
206
207
208 # --- Simple notification observer for demo ---
209
210 class LoggingObserver(FeedObserver):
211 def __init__(self, username: str):
212 self.username = username
213 self.notifications: list[str] = []
214
215 def on_new_post(self, post: "Post") -> None:
216 msg = f"{self.username} notified: new post by {post.author.username}"
217 self.notifications.append(msg)
218 print(f" [Observer] {msg}")
219
220
221 # --- Feed Platform Facade ---
222
223 class FeedPlatform:
224 """Central coordinator. All interactions go through here.
225 Uses fan-out-on-read: feed is assembled at query time from followed users' posts."""
226
227 def __init__(self):
228 self._users: dict[str, User] = {}
229 self._posts: dict[str, Post] = {}
230 self._follow_manager = FollowManager()
231 self._feed_strategy: FeedStrategy = ChronologicalStrategy()
232 self._like_log: dict[str, set[str]] = {} # post_id -> set of user_ids
233
234 def register_user(self, username: str) -> User:
235 user_id = str(uuid.uuid4())[:8]
236 user = User(user_id, username)
237 self._users[user_id] = user
238 return user
239
240 def create_post(self, user_id: str, text: str,
241 timestamp: Optional[datetime] = None) -> Post:
242 author = self._users[user_id]
243 post_id = str(uuid.uuid4())[:8]
244 post = Post(post_id, text, author, timestamp)
245 self._posts[post_id] = post
246 author.add_post(post)
247 # Observer: notify followers
248 self._follow_manager.notify_followers(user_id, post)
249 return post
250
251 def like_post(self, user_id: str, post_id: str) -> None:
252 if user_id not in self._users:
253 raise ValueError("User not found")
254 post = self._posts[post_id]
255 if not post.like(user_id):
256 raise ValueError(f"User already liked this post")
257
258 def unlike_post(self, user_id: str, post_id: str) -> None:
259 post = self._posts[post_id]
260 if not post.unlike(user_id):
261 raise ValueError(f"User hasn't liked this post")
262
263 def comment_on_post(self, user_id: str, post_id: str, text: str) -> Comment:
264 author = self._users[user_id]
265 post = self._posts[post_id]
266 comment_id = str(uuid.uuid4())[:8]
267 comment = Comment(comment_id, text, author)
268 post.add_comment(comment)
269 return comment
270
271 def follow(self, follower_id: str, followee_id: str) -> None:
272 self._follow_manager.follow(follower_id, followee_id)
273
274 def unfollow(self, follower_id: str, followee_id: str) -> None:
275 self._follow_manager.unfollow(follower_id, followee_id)
276
277 def register_observer(self, user_id: str, observer: FeedObserver) -> None:
278 self._follow_manager.register_observer(user_id, observer)
279
280 def set_feed_strategy(self, strategy: FeedStrategy) -> None:
281 self._feed_strategy = strategy
282
283 def get_feed(self, user_id: str, page: int = 0, page_size: int = 20) -> Feed:
284 """Fan-out-on-read: collect posts from all followed users, rank, paginate."""
285 following_ids = self._follow_manager.get_following(user_id)
286 # Gather all posts from followed users
287 candidate_posts: list[Post] = []
288 for uid in following_ids:
289 user = self._users.get(uid)
290 if user:
291 candidate_posts.extend(user.posts)
292 # Rank using current strategy
293 ranked = self._feed_strategy.rank(candidate_posts)
294 return Feed(ranked, page, page_size)
295
296
297 if __name__ == "__main__":
298 platform = FeedPlatform()
299
300 # Register users
301 alice = platform.register_user("alice")
302 bob = platform.register_user("bob")
303 charlie = platform.register_user("charlie")
304
305 # Set up observer for Alice
306 alice_observer = LoggingObserver("alice")
307 platform.register_observer(alice.user_id, alice_observer)
308
309 # Alice follows Bob and Charlie
310 platform.follow(alice.user_id, bob.user_id)
311 platform.follow(alice.user_id, charlie.user_id)
312 print(f"Alice follows Bob and Charlie")
313
314 # Bob and Charlie create posts with staggered timestamps
315 now = datetime.now()
316 p1 = platform.create_post(bob.user_id,
317 "Just shipped a new feature! Really proud of the team.",
318 timestamp=now - timedelta(hours=3))
319 p2 = platform.create_post(charlie.user_id,
320 "Great article on system design patterns. Must read for interviews.",
321 timestamp=now - timedelta(hours=2))
322 p3 = platform.create_post(bob.user_id,
323 "Anyone else excited about the new Python release?",
324 timestamp=now - timedelta(hours=1))
325 p4 = platform.create_post(charlie.user_id,
326 "Coffee and code. The perfect morning routine.",
327 timestamp=now - timedelta(minutes=30))
328
329 # --- Chronological feed ---
330 print("\n--- Alice's feed (Chronological) ---")
331 platform.set_feed_strategy(ChronologicalStrategy())
332 feed = platform.get_feed(alice.user_id, page=0, page_size=10)
333 print(feed)
334 for i, post in enumerate(feed.posts):
335 print(f" {i + 1}. {post}")
336
337 # --- Add engagement: likes and comments ---
338 print("\n--- Adding engagement ---")
339 platform.like_post(alice.user_id, p1.post_id)
340 platform.like_post(charlie.user_id, p1.post_id)
341 platform.like_post(alice.user_id, p2.post_id)
342 platform.comment_on_post(alice.user_id, p1.post_id,
343 "Congrats! What was the hardest part?")
344 platform.comment_on_post(bob.user_id, p1.post_id,
345 "Thanks! The database migration was tricky.")
346 platform.comment_on_post(alice.user_id, p2.post_id,
347 "Bookmarked this. Great share!")
348
349 print(f" p1 (Bob's feature post): {p1.like_count} likes, "
350 f"{len(p1.comments)} comments")
351 print(f" p2 (Charlie's article): {p2.like_count} likes, "
352 f"{len(p2.comments)} comments")
353 print(f" p3 (Bob's Python post): {p3.like_count} likes, "
354 f"{len(p3.comments)} comments")
355 print(f" p4 (Charlie's coffee post): {p4.like_count} likes, "
356 f"{len(p4.comments)} comments")
357
358 # --- Engagement-based feed ---
359 print("\n--- Alice's feed (Engagement-weighted) ---")
360 platform.set_feed_strategy(EngagementStrategy())
361 feed = platform.get_feed(alice.user_id, page=0, page_size=10)
362 print(feed)
363 for i, post in enumerate(feed.posts):
364 print(f" {i + 1}. {post}")
365
366 # --- Pagination demo ---
367 print("\n--- Pagination (page_size=2) ---")
368 platform.set_feed_strategy(ChronologicalStrategy())
369 page_num = 0
370 while True:
371 feed = platform.get_feed(alice.user_id, page=page_num, page_size=2)
372 print(f" Page {feed.page + 1}/{feed.total_pages}:")
373 for post in feed.posts:
374 print(f" - {post}")
375 if not feed.has_next_page():
376 break
377 page_num += 1
378
379 # --- Like deduplication ---
380 print("\n--- Like deduplication ---")
381 try:
382 platform.like_post(alice.user_id, p1.post_id)
383 print("ERROR: should have raised")
384 except ValueError as e:
385 print(f" Expected error: {e}")
386
387 # --- Unfollow ---
388 print("\n--- Unfollow Bob ---")
389 platform.unfollow(alice.user_id, bob.user_id)
390 feed = platform.get_feed(alice.user_id, page=0, page_size=10)
391 print(f" Alice's feed after unfollowing Bob: {len(feed.posts)} posts")
392 for post in feed.posts:
393 print(f" - {post}")
394
395 # --- Observer notifications ---
396 print(f"\n--- Observer notifications ({len(alice_observer.notifications)}) ---")
397 for n in alice_observer.notifications:
398 print(f" {n}")
399
400 # Assertions
401 assert p1.like_count == 2, f"Expected 2 likes, got {p1.like_count}"
402 assert len(p1.comments) == 2, f"Expected 2 comments, got {len(p1.comments)}"
403 assert len(feed.posts) == 2, "After unfollowing Bob, only Charlie's posts"
404 assert feed.posts[0].author.username == "charlie"
405 assert len(alice_observer.notifications) == 4, "4 posts created while following"
406 print("\nAll assertions passed.")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.