Skip to main content

Social Network

Post something, and everyone who follows you should see it without refreshing into a full table scan. The whole design question is: who does the work of getting a post into a follower's feed, and when? Doing it at read time is simple and slow; doing it at write time is the interesting part.

Requirements

Functional

  • A user follows or unfollows another user.
  • A user publishes a text post, visible to their followers.
  • Each user has a feed: posts from people they follow, newest first.
  • A follower is notified the moment someone they follow publishes.

Non-functional

  • Reading a feed must not require scanning every post ever made by every followed user - the fan-out work happens once, at publish time, not on every read.
  • Adding a new way to be notified (push, email, in-app banner) must not require changing User or Post.

Design

Post doesn't know who its followers are and User doesn't loop over anyone else's data to build a feed - publishing a post notifies a list of subscribed observers, and each observer decides what to do with the news. That's Observer doing the one thing it's actually good for: decoupling "something happened" from "here's what happens next."

AuthorSocialNetworkPostFeed (follower)publish(text)1new Post(author, text)2publish()3onNewPost(post)4receive(post)5
  1. 1The author only ever talks to the network facade, never to a follower directly.
  2. 2A post is created once and handed its author’s current follower list.
  3. 3Publishing is the post’s own job - it owns the notify step, not the network.
  4. 4Every follower’s Feed is an observer; the post has no idea Feed exists beyond this interface.
  5. 5The feed prepends the post and trims itself to its cap - nobody outside Feed manages its size.

Feed is one such observer: it just prepends the post to a capped list. A push-notification service could be a second observer on the exact same publish call, with zero changes to SocialNetwork or Post.

Class diagram

«interface»FeedObserver+ onNewPost(post)SocialNetwork- users: Map<userId, User>+ follow(a, b)+ unfollow(a, b)+ publish(user, text): PostUser- id: string- following: Set<User>- followers: Set<User>- feed: FeedPost- author: User- text: string- observers: List<FeedObserver>+ publish()Feed- posts: List<Post>- capacity: int+ onNewPost(post)+ recent(): List<Post>
implementsusescreates
Publishing a post notifies every subscribed FeedObserver; Feed is the only observer implemented here.

Code

import java.util.*;
 
interface FeedObserver {
void onNewPost(Post post);
}
 
class Feed implements FeedObserver {
private final int capacity;
private final Deque<Post> posts = new ArrayDeque<>();
 
Feed(int capacity) {
this.capacity = capacity;
}
 
public void onNewPost(Post post) {
posts.addFirst(post);
while (posts.size() > capacity) {
posts.removeLast();
}
}
 
List<Post> recent() {
return new ArrayList<>(posts);
}
}
 
class User {
final String id;
final Set<User> following = new HashSet<>();
final Set<User> followers = new HashSet<>();
final Feed feed = new Feed(200);
 
User(String id) {
this.id = id;
}
 
void follow(User other) {
following.add(other);
other.followers.add(this);
}
 
void unfollow(User other) {
following.remove(other);
other.followers.remove(this);
}
}
 
class Post {
final User author;
final String text;
private final List<FeedObserver> observers;
 
Post(User author, String text, List<FeedObserver> observers) {
this.author = author;
this.text = text;
this.observers = observers;
}
 
void publish() {
for (FeedObserver observer : observers) {
observer.onNewPost(this);
}
}
}
 
class SocialNetwork {
private final Map<String, User> users = new HashMap<>();
 
User register(String id) {
User user = new User(id);
users.put(id, user);
return user;
}
 
void follow(User follower, User target) {
follower.follow(target);
}
 
void unfollow(User follower, User target) {
follower.unfollow(target);
}
 
Post publish(User author, String text) {
List<FeedObserver> observers = new ArrayList<>();
for (User follower : author.followers) {
observers.add(follower.feed);
}
Post post = new Post(author, text, observers);
post.publish();
return post;
}
}

Design decisions

  • Fan-out happens on write, not on read. SocialNetwork.publish pushes the new post into every follower's Feed immediately. Building a feed at read time by merging every followed user's post history would be simpler to write and unusable at any real follower count - this is the classic push-vs-pull tradeoff, and this page takes the side that keeps reads cheap.
  • Notification is Observer, not a hardcoded call to Feed.prepend. Post.publish notifies a list of FeedObservers; Feed happens to be the only implementation here, but a second observer (an email digest, a push notification) plugs in at the same call site with no change to User, Post, or SocialNetwork.
  • Feed caps its own size instead of trusting callers to trim it. A feed that grows forever turns "prepend a post" into a slow operation years into a user's life; keeping the cap inside Feed.receive means every caller gets the same guarantee for free.
  • Follow/unfollow is a set operation on User, not a Friendship join object. A follow here is one-directional and has no state of its own (no pending/accepted), so a plain Set<User> of followers is the whole model - introducing a relationship class would be structure with nothing to hold.
  • What's missing for a real system: ranking (chronological only here, no relevance scoring), pagination past the in-memory cap, and de-duplicating a post that reaches a user through more than one path (retweets/shares) are all real feed-system concerns that don't change the observer wiring above - they'd sit inside Feed.receive, not around it.
0%0 of 122 pages studied