Code a social network with friends and posts
Open in the interactive app: practice, quiz, mock interview →In one minute
- The core of a social network is not posts; it is the relationship graph and the privacy rules that read it. Friendship is a symmetric edge created by a small state machine (request, accept, decline), and every read of a post asks that graph "may this viewer see this?"
- Check privacy when content is read, not when it is written. Relationships change after a post exists: an unfriend or a block must hide old posts immediately, and only a read-time check can do that.
- Blocking is the strongest relation. It overrides every audience setting in both directions, removes the friendship and any pending requests, and unblocking does not bring the friendship back.
- Keep one
AudiencePolicythat answerscan_view(viewer, post). Feeds, search, profile pages and notifications all call it, so a privacy rule changes in one place. - The problem shape is multi-step consistency (a friendship is two adjacency entries that must agree) plus fan-out (a feed merges many friends' timelines). The same audience check appears in file sharing, where "who can open this document" is also a question about a relationship graph.
Requirements
Functional
- Users register and send friend requests. The recipient accepts or declines. If two people send each other requests, the second request accepts the first.
- Friends can unfriend each other. Any user can block another user, and can later unblock them.
- Users create posts with an audience:
public,friendsoronly_me. - A viewer's feed shows their own posts and their friends' posts that they are allowed to see, newest first.
- Show a user's friends, the mutual friends of two users, and "people you may know" ranked by the number of mutual friends.
Non-functional
- Privacy is never stale. After an unfriend or a block, the next read already reflects it.
- Friendship is always symmetric. No state exists where A lists B as a friend but B does not list A.
- Graph queries (is X a friend of Y, mutual friends) are fast enough to run on every read.
Out of scope
- The distributed feed architecture (fan-out on write, ranking, caching). Design Facebook News Feed and Design Instagram cover those. This lesson is the in-process object model they sit on.
- Groups, pages, followers without friendship, messaging, and custom friend lists. We note where each plugs in.
Entities and relationships
User has an id, a display name and a privacy default. Users are nodes; everything interesting is an edge.
FriendRequest links a sender to a recipient with a status: pending, accepted, declined or cancelled. Only pending requests drive behaviour; the rest are history, useful for abuse analysis.
Friendship is the symmetric edge. One record keyed by the ordered pair (smaller id, larger id) makes "exactly one edge" true by construction; two adjacency entries, one in each user's friend set, make "list my friends" and "are we friends?" cheap. In memory we use adjacency sets and update both sides in one critical section.
Block is a directed edge from the blocker to the blocked user, but its effect is symmetric: neither side sees the other. It is directed because only the blocker can lift it.
Post has an id, an author, text, a timestamp and an Audience. Comment and Reaction hang off a post and inherit its audience.
AudiencePolicy is the single place that answers "may viewer V see post P?" SocialNetwork is the facade with one method per user action, coordinating the graph, the posts and the policy.
Class diagram
classDiagram
class User {
+id
+name
}
class FriendRequest {
+from
+to
+status
+accept()
+decline()
}
class SocialGraph {
+areFriends(a, b) bool
+isBlocked(a, b) bool
+friendsOf(u) set
+befriend(a, b)
+sever(a, b)
}
class Post {
+id int
+author
+text
+ts int
+audience Audience
}
class Audience {
<<interface>>
+allows(viewer, author, graph) bool
}
class PublicAudience
class FriendsAudience
class OnlyMeAudience
class AudiencePolicy {
+canView(viewer, post) bool
}
class SocialNetwork {
+sendRequest(from, to) string
+accept(user, requester)
+block(a, b)
+post(author, text, audience, ts) int
+feed(viewer, limit) list
+suggestions(user, k) list
}
Audience <|.. PublicAudience
Audience <|.. FriendsAudience
Audience <|.. OnlyMeAudience
Post --> Audience
Post --> User
FriendRequest --> User
SocialGraph --> User
AudiencePolicy --> SocialGraph
AudiencePolicy --> Audience
SocialNetwork --> SocialGraph
SocialNetwork --> AudiencePolicy
SocialNetwork --> PostKey flows
The flow that shows why privacy is read-time is "Bea unfriends Ali, then Ali opens his feed". Nothing is deleted from Ali's feed when Bea unfriends him. The feed is assembled on read, and each candidate post passes through the policy, which reads the graph as it is now.
sequenceDiagram
participant B as Bea
participant A as Ali
participant S as SocialNetwork
participant G as SocialGraph
participant P as AudiencePolicy
B->>S: unfriend(bea, ali)
S->>G: sever(bea, ali)
G-->>S: both adjacency sets updated
A->>S: feed(ali, 20)
S->>G: friendsOf(ali)
G-->>S: {cem}
S->>S: candidates = posts by ali and cem
loop each candidate
S->>P: canView(ali, post)
P->>G: isBlocked, areFriends
P-->>S: true or false
end
S-->>A: newest 20 visible postsBea's friends posts are no longer candidates, and if Ali opens one by a direct link, the policy rejects it too. One graph change closes both doors.
Crossing requests are the other interesting flow. If Bea sends Ali a request while his request to her is pending, the second send_request turns the pair into a friendship. Without this rule, both users see "request sent" and wait for each other.
Design patterns used and why
Strategy for audiences. PublicAudience, FriendsAudience and OnlyMeAudience each implement allows(viewer, author, graph). Adding friends_of_friends is a new class, not a new branch in if chains spread across feed, profile and search code. The policy wraps the strategies with rules common to all of them: the author always sees their own post, and a block always wins.
Specification for composite rules. "Friends, except Dan" combines predicates. A Specification (and, or, not over small predicates) expresses that without a class per combination. Start with Strategy; move to Specification when product asks for exclusions.
State for friend requests. A request moves from pending to accepted, declined or cancelled and never back. Encoding that as a state machine stops bugs like accepting a request that was already declined because two devices were open.
Facade. SocialNetwork gives controllers one call per user action. Block touches the friendship, both request directions and the block set; callers should not coordinate those.
Observer for side effects. FriendshipCreated and PostCreated events feed notifications, the suggestions index and the feed cache, which can be slow or fail without blocking the action.
Concurrency and edge cases
Two sides of one friendship
Accepting a request writes two adjacency entries and deletes one pending request. In memory, do all three under one lock (or a lock per user pair, acquired in id order so two concurrent operations on the same pair cannot deadlock). In a sharded store the two users often live on different shards. Facebook's TAO paper describes exactly this: a friend edge has a configured inverse type, TAO writes the inverse first and then the forward edge, does not make the two writes atomic, and repairs any hanging association with an asynchronous job. That is a deliberate trade: symmetric edges are cheap to read and occasionally need repair.
Crossing requests and double taps
Two send_request calls between the same pair can race. Under a pair lock, the second one either sees the first (and auto-accepts or reports already_requested) or runs first itself. In a database, a unique index on the ordered pair for pending requests gives the same guarantee.
Block beats everything
A block removes the friendship and both pending requests, and makes every post invisible in both directions, including public ones. It also prevents new requests in both directions. Unblocking removes only the block edge. Restoring the friendship automatically would surprise the blocked person, who may have been blocked for a reason.
Audience changes and comments
Narrowing a post from public to friends takes effect on the next read automatically; a feed cache must still filter on read or be invalidated by an AudienceChanged event. A comment is visible only to its post's audience, whatever the commenter's own settings, or comments become a side channel that leaks the post.
In the wild
- Facebook TAO, 2013: inverse edges can hang. TAO stores an association and its inverse as two writes that may land on different shards. If the second write fails, a "hanging" association is left behind, and an asynchronous job finds and repairs it. This is the repairable-symmetry choice made at scale (USENIX ATC 2013).
- LinkedIn LIquid: the graph leaves the application. LinkedIn found that serving degree and connection queries from general-purpose stores was too slow and awkward, so it built an in-house graph database to answer multi-hop queries directly (LinkedIn Engineering).
- Slack, 2022: an emptied cache melts the database. A Consul agent rollout replaced memcached nodes with empty ones, and a scatter query overloaded the database. A cache of graph or feed results needs the same protection against a cold start (Slack Engineering).
Down the stack
Layer 1The contract
For any two users, are_friends(a, b) == are_friends(b, a). At most one pending request exists per ordered pair, and never two crossing ones. can_view(v, p) is true when v is the author. Otherwise it is false whenever either user blocks the other, and otherwise it follows the audience. Every read, including feeds, applies can_view against the graph as it stands at that moment. A rejected action changes nothing.
Layer 2The mechanism
Friend sets are hash sets, so "are we friends?" is one O(1) membership test, and mutual friends is an intersection costing O(min(|A|, |B|)). "People you may know" counts, for each friend f, every friend of f who is not u, not a friend and not blocked. That costs the sum of the friends' degrees: 300 friends with 300 friends each is about 90,000 increments, so production systems precompute candidates offline.
The feed is a k-way merge of per-author lists already in time order. Push each author's newest post onto a max-heap keyed by (ts, id), pop the top, check can_view, and push that author's next post. Twenty results from 300 friends cost about 170 heap comparisons plus 20 policy checks, however much each friend has posted. TAO keeps association lists newest first for the same reason: assoc_range reads the head without scanning the rest.
Layer 3The machine
A CPython set of 500 friend ids reports about 32 KB from sys.getsizeof (a 2,048-slot table at 16 bytes per slot, hash plus pointer), before the id strings themselves. A Go map[string]bool stores a 16-byte string header and a byte per entry in 8-entry buckets, roughly 15 KB for 500 friends. Facebook caps a profile at 5,000 friends, where the Python set is about half a megabyte. A membership test touches one or two cache lines, about 50 to 100 ns on a cache miss. A policy check is therefore far cheaper than the network hop that fetched the post. A heap of 300 tuples is a contiguous array, so the merge stays in L1 and L2 caches.
Why it's built this way
Relationships change constantly and privacy mistakes are expensive, so privacy is evaluated on read against the live graph. Read-time checks run on every post shown, so the graph must answer membership in O(1), which means hash sets keyed by user. Symmetric friendship read from either side needs two adjacency entries, and two entries can disagree, so writes to both sides go under one lock in memory or get a repair job across shards. Feeds read only the newest few posts of many authors, so per-author lists are kept sorted and merged with a heap instead of sorted from scratch. This is the shape Facebook's TAO stores: objects, typed associations with inverse edges, and time-ordered association lists. Google's Zanzibar answers the same "may this user see this?" question from relationship tuples.
Extensibility
- Followers (asymmetric edges). A
followsedge type with no request flow; the feed also merges followed authors. The policy is unchanged, becausePublicAudiencealready allows anyone not blocked. - Friends of friends and custom lists. New
Audiencestrategies: one checks whether friend sets intersect, the other holds a set of user ids, with exclusions as Specifications. - Groups. A group is a node with membership edges, and its posts use a
GroupAudience. - Moving to a store. Replace
SocialGraph's sets with a repository over an edge table keyed by (user, type, other user, time), TAO's association model. The policy and facade do not change.
Variant: professional networking (LinkedIn-style)
A professional network keeps the same graph machinery but changes the relationship rules and the read that matters most.
Two relationships, not one. A connection is symmetric and needs consent, like friendship. A follow is one-way and needs none, and connecting implies following in both directions. Model Connection and Follow as separate types, because they have different rules. One Relationship class with a type flag ends up as a pile of if statements.
A richer invitation. The invitation is a State pattern object (pending, accepted, ignored, withdrawn) and carries a note and a sender-side rate limit over a rolling week, so the network cannot be used for bulk outreach. A withdrawn invitation does not refund its slot, and a rejected attempt consumes nothing. Ignoring is not blocking: decide whether the sender may retry at once, after a cool-down, or never. Crossing invitations resolve as in friend requests: the second one accepts the first. Accept and withdraw race, so both are conditional updates on status = 'PENDING', and exactly one wins.
Degree is the new core query. Every profile and search result shows 1st, 2nd, 3rd or out of network, plus mutual connections. Degree is a breadth-first search capped at three hops. Searching from both ends and expanding the smaller frontier turns a three-hop question into two short expansions that meet in the middle (bidirectional search). Mutuals are a set intersection that iterates the smaller set and probes the larger. The cost explodes with depth: with around 500 connections each, second degree is up to 250,000 ids and third reaches tens of millions. So a page view never runs a full search. Production systems cache second-degree sets and accept seconds of staleness. At hundreds of millions of members the adjacency leaves the process entirely; LinkedIn built its own graph database for this (LIquid).
Different consistency budget. Privacy here is mostly profile visibility by degree, so a stale degree badge is cosmetic, unlike a stale friends-only post. Accept must still create the edge and close the invitation together, in one transaction or as an idempotent edge write driven by invitation state. Everything downstream (feeds, counters, suggestions) runs on a ConnectionCreated event after commit and may lag.
What carries over. Blocks still override everything, and "people you may know" still ranks second-degree members by mutuals. A block also removes a member from any degree path.
Leadership lens
- The design review question is "show me every code path that returns a post, and prove each one calls
can_view." Search, notifications, link previews and data exports are the usual leaks. Enforce it with a single repository method that returns only already-filtered posts. - Treat privacy rules as a product contract with tests: a table of (relationship, audience, expected visibility) run in CI. Block and unfriend cases belong in that table, not in ad hoc unit tests.
- Decide early whether symmetric edges are atomic or repairable. Repairable is cheaper at scale, but it needs an owner for the repair job and an alert on its backlog.
- "People you may know" is a growth feature with a privacy cost. It can reveal that two people are connected. Have legal and product sign off on what signals it uses, and always respect blocks.
Check yourself
Why not filter posts at write time by copying each post only into the feeds of people allowed to see it?
Show answer
The allowed set changes after the post is written: an unfriend, a block or an audience edit would leave stale copies that still show the post. A read-time check against the live graph is the only way to make those changes take effect on the next read.
Ali and Bea each send the other a friend request at almost the same moment. What should the system end up with?
Show answer
One friendship and no pending requests. Whichever request is processed second sees the opposite pending request and accepts it instead of creating a crossing request.
Bea blocks Ali, then unblocks him a day later. Are they friends? Can Ali see Bea's public posts?
Show answer
They are not friends, because the block severed the friendship and unblock does not restore it. Ali can see Bea's public posts again, because only the block was hiding them.
Why does a comment on a friends-only post not become visible to the commenter's own friends?
Show answer
A comment inherits the post's audience. Showing it to a wider audience would leak the existence and context of the post.
Sources and further study
Primary
- TAO: Facebook's Distributed Data Store for the Social Graph (USENIX ATC 2013): objects, associations, inverse edges and repair of hanging associations.
- Zanzibar: Google's Consistent, Global Authorization System: relationship-based access checks at scale.
Engineering blogs
- LIquid: The soul of a new graph database, Part 1 (LinkedIn Engineering): why LinkedIn built its own graph database.
- How LIquid connects everything (LinkedIn Engineering): which member features run on the graph service.
- Slack's incident on 2-22-22: cold caches and scatter queries.
Reference
- Relationship-based access control (Wikipedia): deciding access from the relationship graph.
- Bidirectional search (Wikipedia): the meet-in-the-middle idea behind degree queries.
Books
- Design Patterns, Gamma, Helm, Johnson and Vlissides: chapter 5 (Behavioral, including State and Strategy).
- Designing Data-Intensive Applications (1st ed.), Martin Kleppmann: Ch 2 (Data Models and Query Languages, graph-like models).
Watch
- System Design 13: Design Social Media like Facebook (InterviewWithBunny): covers both the high-level design and the class-level model of a Facebook-style network.
- Facebook System Design (codeKarle): the distributed architecture that this object model sits inside, including feed generation.
- Design Scalable News Feed System (Irtiza Hafiz): builds a feed from a simple version up to an optimised one, a good companion to the k-way merge.
Free resources
- TAO: Facebook's Distributed Data Store for the Social Graph (USENIX ATC 2013): objects, associations, inverse edges, time-ordered association lists and the repair of hanging associations.
- TAO: The power of the graph (Engineering at Meta): a shorter introduction to the same system.
- Zanzibar: Google's Consistent, Global Authorization System: relationship-based access checks at scale, the same "can this user see this?" question applied to documents.
- Relationship-based access control (Wikipedia): background on deciding access from the relationship graph.
- Specifications (Evans and Fowler): the pattern for composing audience rules such as "friends except these people".
- State pattern (Refactoring.Guru): the pattern behind the friend request lifecycle.