Courses › System Design › Question Breakdowns

Design Meta Post Privacy

Read-time audience checks, friends-of-friends as set intersection, caching inputs, and every place a post can leak.

Design the system that decides who can see a post on a social network like Facebook: Public, Friends, Friends of friends, Only me or a custom list, plus blocks. Every feed load, profile view, search result and notification must respect it. It's a question about authorization at read time, graph queries at massive scale, caching without leaking, and what happens when settings or friendships change.

Requirements

Functional

  • Authors choose a post's audience: Public, Friends, Friends-of-friends, Only me, or Custom (include/exclude specific people or lists)
  • Every surface (feed, profile, direct link, search, notifications, comments) shows a post only to viewers allowed to see it
  • Authors can change a post's audience later, and blocks always win
  • Changes to friendships or audience take effect quickly (seconds)

Out of scope: building the feed ranking itself, group/page permissions, content moderation.

Non-functional

  • Correctness first: never show a post to someone who shouldn't see it. Showing too little is a bug; showing too much is a privacy incident
  • Low latency: a feed request checks hundreds of candidate posts, so each check must take ~1ms or less
  • Scale: 3B users, ~500 friends on average (cap 5,000), and hundreds of millions of post checks per second at peak
  • Fresh: unfriending or blocking takes effect within seconds

[!TIP] Enforce on read, not on write You *could* precompute "who can see this post" when it's written. But audiences change, friendships change, and friends-of-friends can be millions of people. The standard answer is a central privacy check at read time — canView(viewer, post) — that every surface calls, backed by fast graph lookups and careful caching.

Capacity estimates

Core Entities

  • Post — ID, author, audience type, custom include/exclude lists, created at
  • Friendship — an undirected edge (stored as two directed edges, sharded by user)
  • Block — blocker, blocked
  • Friend list — owner, name, members (for custom audiences)

API

The privacy service is internal; every product surface calls it:

POST /internal/privacy/check
{ "viewerId": 42, "postIds": [9001, 9002, 9003, ...] }
→ { "visible": [9001, 9003] }

PATCH /v1/posts/{postId}/audience   { "audience": "friends_of_friends" }
POST  /v1/blocks                    { "userId": 77 }

Batching matters: a feed asks about hundreds of posts at once, which lets the service fetch the viewer's friend set once and reuse it.

High-Level Design

viewer = client "Viewer" @0,1
feed = service "Feed / Profile /\nSearch Services" @1,1
priv = service "Privacy Check\nService" @2,1
posts = cache "Post Metadata\n(audience, author)" @3,0
graph = cache "Graph Cache\n(friend sets, blocks)" @3,1
gdb = store "Social Graph DB\n(sharded by user)" @4,1
lists = store "Custom Lists" @3,2
inval = queue "Change Events" @4,2
viewer -> feed
feed -> priv : batch check
priv -> posts
priv -> graph
priv -> lists
graph -> gdb : on miss
gdb --> inval : friend / block changes
inval --> graph : invalidate
  • A surface gathers candidate posts and calls the privacy service with the viewer and post IDs.
  • The service loads each post's audience and author, plus the viewer's friend set and block list, from caches backed by the graph store (a TAO-like cache over sharded MySQL).
  • It evaluates the rules per post and returns the visible subset. The surface filters before rendering, counting or notifying.
  • Graph changes publish events that invalidate cached friend sets and blocks.

Deep Dives

1. Evaluating each audience

Order the rules so the cheapest and strongest come first:

Blocks are always evaluated and always deny.

2. Friends of friends without traversing millions of nodes

Expanding friends-of-friends (500 × 500 = 250k people, or far more for popular users) on every check is too slow. Instead ask the narrower question: do the viewer and author share at least one friend?

  • Load both friend sets (the viewer's once per batch; the author's from cache) and intersect. With sorted ID arrays or bitmaps (Roaring), intersecting two 500-element sets is microseconds.
  • High-degree authors (5,000 friends) are still fine; celebrities usually post Public or use follower models instead.
  • For very hot authors, cache fof(viewer, author) results with a short TTL.

[!WARNING] Don't cache "visible posts per viewer" Caching the *output* (which posts viewer X can see) means every friendship or audience change has to find and invalidate an unbounded set of entries. Cache the inputs (friend sets, blocks, post audiences), whose invalidation is local and simple, and recompute the cheap decision every time.

3. Changes: unfriend, block, audience edits

  • Audience change: update the post row and invalidate its cached metadata; the next check uses the new audience. Because enforcement is at read time, nothing else needs rewriting.
  • Unfriend / block: write the graph change, then publish an event that invalidates both users' cached friend sets and blocks in every region. Target: seconds.
  • Read-your-writes for the actor: after Alice blocks Bob, *Alice's* next request should reflect it immediately — route her reads to the primary, or carry a version token she just wrote.

4. Everywhere a post can leak

The check is only as good as its coverage. Common leaks interviewers probe:

  • Precomputed feeds: fan-out on write may have put the post in a feed before the audience was narrowed. That's fine *only* because the feed re-checks at read time.
  • Counts and previews: "12 comments" or a notification preview can reveal a post's existence. Run the check before counting or notifying.
  • Search indexes: index the post with its audience fields, filter coarsely in the index (public vs. not), then run the exact check on the result page.
  • Reshares: a reshare can never widen the original's audience; the check on a reshare includes the original post.
  • Caches and CDNs: never cache a non-public post's rendered content under a URL that doesn't include the viewer's identity.

5. Failure mode: fail closed

If the graph cache or privacy service is unavailable, hide non-public posts rather than show them. Degraded feeds are an outage; leaked posts are a breach.

6. Scaling and locality

  • Shard the graph by user ID; replicate it to every region with a read-through cache tier (Facebook's TAO is exactly this).
  • Co-locate the privacy check with the feed service (a library + cache client, or a sidecar) to avoid an extra network hop per batch.
  • Hot users' friend sets are cached in every region; changes propagate as invalidations through the replication stream.

What's expected at each level

  • Mid-level: an audience field on posts, a canView check that reads friendships, and enforcement on feed and direct-link reads.
  • Senior: read-time enforcement with batched checks, friends-of-friends as a set intersection, caching inputs not outputs, block precedence, invalidation on changes.
  • Staff: auditing every surface for leaks (counts, notifications, search, reshares), fail-closed behavior, read-your-writes for privacy changes, global replication of the graph and its invalidations.