•CASE STUDY

Post Privacy and Visibility System (Facebook-style)

4 min read·700 words·Advanced

Asked at

1 candidate report in Feb 2026

How to use this case study

SDE-2 / Mid

Define the privacy options (Public, Friends, Friends-of-friends, Only me, Custom) and a check(viewer, post) function

SDE-3 / Senior

  • Enforce privacy everywhere posts appear (feed, profile, search, notifications, shares)
  • With fast friend-graph lookups and caching

Staff / Principal

  • Handle changes (unfriending, privacy edits) consistently
  • Filtering at scale in feeds and search
  • Auditing for privacy bugs

Problem RestatementProblem

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.

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.

Data and ArchitectureArchitecture

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
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.

Deep Dive — Enforcing the audience on every surfaceDeep dive

A post is "Friends only". That has to hold in the feed, on the profile, in search, in notifications, in shares and in the API. One surface that forgets is a privacy incident.

Weak

Filter in the feed service

The feed applies the visibility rule as it assembles posts.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  POST["Friends-only post"] --> FEED["Feed - filtered correctly"]
  POST --> PROF["Profile page - separate code path"]
  POST --> SRCH["Search results - separate code path"]
  POST --> NOTIF["Notification preview - separate code path"]
  PROF --> LEAK["Any one of these missing the check leaks the post"]
  SRCH --> LEAK
  NOTIF --> LEAK

The rule lives where someone remembered to put it. Every new surface is a new chance to omit it, and the omission is invisible until someone notices their private post somewhere public.

Good

A shared visibility library

Extract one canSee(viewer, post) function and call it from each surface.

The logic is now correct and consistent wherever it is called — a real improvement. What it cannot enforce is being called. A new endpoint, a data export, an internal tool or an analytics job can still read the table directly, and nothing fails.

Best

One check, applied at every read boundary, with the index shaped to help

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  CAND["Candidate posts generated broadly"] --> BATCH["Batch canSee(viewer, posts) before results are returned"]
  BATCH --> RANK["Rank only what survives"]
  SRCH["Search"] --> SPLIT["Public posts in a public index"]
  SPLIT --> FR["Friends-only: filter by author in viewer's friend set at query time"]
  FR --> EXACT["Then the exact check on the final page"]
  SHARE["Reshare"] --> NARROW["Audience of a share can never widen the original"]
  CACHE["Rendered feed"] --> NEVER["Never cached across viewers"]
  • Filter before ranking, in a batch. Visibility is a precondition, not a ranking signal — and batching keeps it one call per page instead of one per post.
  • Partition the search index by audience. Public posts live in a public index; restricted posts are narrowed at query time by the viewer's friend set, then confirmed by the exact check on the returned page. Filtering after retrieval alone means private content has already been scored and counted.
  • A share can never widen the audience. Resharing a friends-only post makes it visible to people who could already see it, never beyond — the most common way privacy is lost, because it looks like a feature.
  • Never cache a rendered feed across viewers. A cache key without the viewer in it is the fastest possible route to showing one person another person's private posts.

The structural point: make the check the only way to read posts — a data access layer that requires a viewer — so bypassing it is impossible rather than merely discouraged.

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.

Wrap-UpWrap-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.

More Case Studies

Frequently Asked Questions

What is the Post Privacy and Visibility System (Facebook-style) system design question?

Post Privacy and Visibility System (Facebook-style) is a system design interview question asked at FAANG companies. It covers security, distributed systems, caching, search and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Post Privacy and Visibility System (Facebook-style) question?

Meta have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Post Privacy and Visibility System (Facebook-style) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Post Privacy and Visibility System (Facebook-style) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →