0) Problem Restatement
Meta asked: design the privacy system for social media posts. Each post has an audience:
- Public: anyone.
- Friends: only the author's friends.
- Friends of friends (optional).
- Only me.
- Custom: specific people or lists allowed, with some excluded ("Friends except Alex").
Privacy must be enforced everywhere posts show up: news feed, profile pages, search, notifications, shares and comments, and must react correctly when someone unfriends or the author changes the setting.
1) The Core Check
can_view(viewer, post):
if viewer == post.author: return True
if viewer blocked by / blocking author: return False
switch post.audience:
PUBLIC: return True
ONLY_ME: return False
FRIENDS: return are_friends(viewer, author)
FRIENDS_OF_FRIENDS:return are_friends(viewer, author) or share_friend(viewer, author)
CUSTOM: return (viewer in allow_list or viewer in allowed_lists) and viewer not in deny_list
Rules to state: deny beats allow, blocking beats everything, and the author can always see their own post.
2) Data and Architecture
Architecture Diagram
flowchart LR
SVC["Feed / profile / search / notifications"] --> PC["Privacy check service - batch API"]
PC --> FG[("Friend graph - cached adjacency")]
PC --> PA[("Post audience - stored with post")]
PC --> BL[("Block lists")]
PC --> CL[("Custom lists")]
ED["Privacy edits / unfriend events"] --> INV["Cache invalidation"]
INV --> PC- Post audience is stored with the post:
audience_type,allow_ids,deny_ids,allowed_list_ids. - Friend graph: a sharded graph store with an in-memory cache of each user's friend set.
are_friends= a set lookup. "Friends of friends" = check whether the two friend sets intersect (bounded work, with caching). - Privacy check service exposes a batch API (
can_view(viewer, [post_ids])), because feeds check hundreds of posts at once.
3) Enforcing It Everywhere
- Feed: candidate posts are generated broadly, then filtered with the batch check before ranking results are returned. Never cache a rendered feed across viewers.
- Profile: the same check for each post on the page.
- Search: the index stores audience data. Public posts go in a public index. For friends-only posts, filter at query time using the viewer's friend set (e.g., author ∈ viewer's friends), then run the exact check on the final page of results.
- Notifications and shares: a share can never widen the original audience. A reshared "Friends" post is visible only to people who can see the original. Comments inherit the post's audience.
- Defense in depth: the final response layer re-checks items, so a bug in one service doesn't leak data.
4) Changes and Consistency
- Author changes the audience: update the post's audience, and invalidate caches and precomputed feeds that included it. Future requests use the new setting immediately, since checks happen at read time.
- Unfriend: update the friend graph, and invalidate both users' friend-set caches. Friends-only posts stop showing right away, because checks read the current graph.
- Why check at read time: precomputing "who can see what" is huge and goes stale. A fast check with cached friend sets is simpler and always current.
5) Wrap-Up
Store each post's audience (type + allow/deny lists) with the post, and evaluate a clear can_view rule set (author sees own, blocks and denies win, then public, friends, friends-of-friends or custom lists) against a cached, sharded friend graph through a batch privacy service. Enforce it at read time in every surface (feed, profile, search, notifications, shares), never let shares widen the audience, re-check at the response layer, and invalidate caches on audience changes and unfriending.