Designing a News Feed System for 500 Million Daily Active Users

Designing a News Feed System for 500 Million Daily Active Users

Designing a News Feed System for 500 Million Daily Active Users

A complete, from-first-principles walkthrough of how to design a news feed — like the ones behind Facebook, Instagram, X (Twitter), and LinkedIn — that stays fast and fair even when some accounts have over 100 million followers.

01

Introduction and History

Open any social app today — Instagram, X (formerly Twitter), LinkedIn, Facebook — and the first thing you see is a scrolling list of posts from people and pages you follow. That scrolling list has a name: the news feed. It looks simple. You open the app, you scroll, new posts appear. But underneath that simple scroll is one of the hardest problems in distributed systems, and it is a favorite topic in system design interviews for a very good reason: it touches almost every important idea in backend engineering at once — storage, caching, queues, ranking, consistency, and scale.

Let us start with a bit of history, because it helps explain why the problem is shaped the way it is today. Before news feeds existed, social platforms worked more like a personal profile page. On early Friendster and MySpace, you visited someone’s page to see what they posted. There was no single place that aggregated everything happening across all your friends. You had to go looking for updates, one profile at a time.

That changed in September 2006, when Facebook launched a feature called News Feed. Instead of making you visit each friend’s page, Facebook pulled updates from everyone you were connected to into one continuously updating stream on your home page. It was controversial at first — many users felt it was an invasion of privacy, since actions that used to be quiet (like changing your relationship status) were now broadcast automatically to everyone who knew you. But within a few years, the feed model became the default way people consumed social content, and every major platform since — Twitter in 2006, Instagram in 2010, LinkedIn’s feed, TikTok’s “For You” page — has built some version of the same core idea: take content produced by a network of people, and assemble it into a personalized, ranked stream for every single viewer.

The interesting part, and the part interviewers care about most, is that this feature quietly forces you to solve a really hard distributed computing problem: if millions of people are producing content at the same time, and hundreds of millions of people are reading personalized combinations of that content at the same time, how do you make both directions fast?

Era 1

Profile-Page World

Friendster, early MySpace — you visited each friend’s page one at a time; no unified stream existed.

Era 2

The Aggregated Feed

September 2006, Facebook News Feed — updates from everyone you follow pulled into one home-page stream.

Era 3

Personalized, Ranked, Global

Twitter, Instagram, LinkedIn, TikTok “For You” — the same core idea, refined into ML-ranked streams at planetary scale.

Simple analogy

A news feed is like a personal newspaper that is freshly edited for you every time you open the app — assembled in milliseconds from thousands of writers you follow, ranked by what you care about most, and reprinted the moment anything new arrives. The magic is that every one of the 500 million readers gets their own private edition, simultaneously.

💬
What an interviewer may ask

“Why is a news feed considered a hard system design problem? Isn’t it just ‘show me the latest posts from people I follow’?” A strong answer explains that the difficulty is not in reading a list — it is in the fan-out: the number of people who need to see a single post can range from zero to over a hundred million, and the system has to handle that entire range efficiently, in real time, without falling over when someone with 100 million followers hits “post.”

02

Problem and Motivation

Let us state the problem the way an interviewer would state it, and then break down why it is genuinely difficult.

📌
Problem statement

Design a news feed system for a social network with 500 million daily active users (DAU). Users follow other users. When a user posts, everyone who follows them should be able to see that post in their personalized feed, ranked and ready to load quickly. Some accounts — celebrities, public figures, large media pages — have more than 100 million followers.

On the surface, this sounds like a database query: “give me the latest posts from everyone I follow, sorted by time.” And for a small app with a few thousand users, that is genuinely all you would need — a simple SQL join between a follows table and a posts table would do the job in milliseconds.

The trouble starts when you scale two numbers at once: the number of people producing content, and the number of followers a single account can have. At 500 million daily active users, if even a modest fraction of them post regularly, you are looking at tens of thousands of new posts every second at peak times. Read that against a feed load: every one of those 500 million users might refresh their feed several times a day, which means the read traffic is enormously larger than the write traffic — often by a factor of 100 to 1 or more in social systems. A system that is naively built to “query on read” will get crushed by that read volume the moment it has any real number of users, because every single feed load would require scanning the posts of everyone that user follows, sorting them, and returning the top results — repeated hundreds of millions of times a day.

Now add the celebrity problem, which is the detail that makes this specific prompt interesting. Imagine a naive fix: whenever someone posts, immediately copy that post into the personal feed of every follower, so reads become instant (you just read your own pre-built feed). This is called fan-out-on-write, and we will cover it in depth soon. It works beautifully for a normal user with 300 followers — the system does 300 small writes, done in a fraction of a second. But if a celebrity with 100 million followers posts a single photo, the system suddenly has to perform 100 million individual writes, one for each follower’s feed, just because one person clicked “post.” Do that a few times a day, across a handful of extremely large accounts, and you can bring an entire fan-out pipeline to its knees — a phenomenon engineers at Twitter genuinely nicknamed the celebrity problem or the “Justin Bieber problem,” after episodes where a hugely followed account posting would spike system load dramatically.

So the real engineering problem being tested here is not “how do you build a feed” — it is “how do you build a feed that behaves gracefully at both extremes of the follower distribution, from an account with three followers to an account with a hundred million, without one extreme punishing the design decisions made for the other.”

Real-life analogy

Imagine a postal system where a normal person sends 300 birthday cards a year — the post office handles it easily. Now imagine one person decides to send 100 million cards in a single afternoon. Even if you have plenty of mail trucks, the sheer act of stamping, sorting, and delivering that avalanche of mail at once will paralyze the whole network for hours. Every other person’s birthday card is now stuck behind that one giant batch. That is exactly the celebrity fan-out problem in one image.

💬
What an interviewer may ask

“What happens if you just query posts on demand every time a user opens the app, instead of pre-computing feeds?” Good answer: this is called the pull/read-time model. It avoids the celebrity fan-out cost entirely because nothing is copied anywhere, but it shifts the cost to read time — every feed load now has to fetch and merge posts from potentially thousands of followed accounts, which is slow and expensive at read volumes in the billions per day. The right answer is rarely “always push” or “always pull” — it is knowing when each one wins, which is exactly what the hybrid approach later in this tutorial solves.

03

Requirements and Capacity Estimation

Before drawing a single box in a diagram, a good system designer always writes down what the system actually needs to do, and roughly how big it needs to be. Skipping this step is one of the most common mistakes candidates make in interviews — they jump straight to drawing microservices without ever stating what “fast” or “scalable” actually means in numbers.

3.1 Functional requirements

FR1

Create posts

Users can create posts (text, images, video, links).

FR2

Follow / unfollow

Users can follow and unfollow other users, forming the underlying social graph.

FR3

Personalized feed

Users can view a personalized, ranked feed made of posts from people they follow.

FR4

Engagement

Users can like, comment on, and share posts, and those interactions should influence ranking.

FR5

Near real-time updates

Feed should update with new content without requiring a full page reload.

3.2 Non-functional requirements

NFRTarget / Rationale
Low latency readsA feed should load in well under 200 milliseconds for the vast majority of requests.
High availabilityThe feed is a core product surface — it should stay available even during partial infrastructure failures, favoring availability over strict consistency for most operations (we connect this to CAP later).
Eventual consistency acceptableIt is fine if a brand-new post takes a few seconds to reach every follower’s feed. It is not fine if the whole system goes down.
High write throughputThe system must absorb bursts of posting activity, especially from very large accounts, without cascading failure.
Horizontal scalabilityThe design should scale horizontally as user count and follower-graph density grow.

3.3 Back-of-the-envelope capacity estimation

Interviewers love this part because it separates candidates who can reason about scale from candidates who only know buzzwords. Let us work through realistic numbers for our 500 million DAU platform.

Users

500,000,000 DAU

Daily active users forming the base traffic estimate.

Posts

~ 250M / day

Assuming an average of 0.5 posts per user per day.

Avg writes

~ 2,900 / sec

250 million posts spread across 86,400 seconds.

Peak writes

~ 14,500 / sec

Roughly 5x the average during evening/prime posting hours.

Followers

~ 200 avg

Typical follower count per user — the “normal” case for fan-out.

Celebrities

> 10M followers

A few thousand accounts — the tail of the distribution that drives all the design pain.

Now the read side. Assume each of the 500 million daily active users opens their feed an average of 10 times a day (a conservative number for a habit-forming social app).

MetricCalculationResult
Total feed reads per day500M users × 10 opens5 billion reads/day
Average reads per second5,000,000,000 ÷ 86,400≈ 58,000 reads/sec
Peak reads per second5× average (evening peak)≈ 290,000 reads/sec
Read : Write ratio58,000 : 2,900roughly 20 : 1

That last row is the single most important number in this entire design. A 20:1 read-to-write ratio (in real systems like Twitter, the ratio has historically been closer to 100:1 or higher) tells you immediately: optimize aggressively for read performance, even if it costs you extra work at write time. This one number is the entire justification for fan-out-on-write, caching pre-built feeds, and every other read-optimization technique you will see in this tutorial. It is also why the celebrity problem is such a headache — it is the one case where a write briefly turns into millions of reads’ worth of work, right at the moment it happens.

💾
Storage estimation

If each post (metadata only, not media) takes roughly 1 KB to store, then 250 million posts per day is about 250 GB of new metadata daily, or roughly 90 TB a year — manageable for a modern distributed database, but it tells you immediately that you need horizontal partitioning (sharding) rather than a single machine, and a separate, larger storage tier (like blob storage) for the actual images and videos, which dominate total storage far more than text metadata does.

💬
What an interviewer may ask

“Why does the read-to-write ratio matter so much for this design?” Because it tells you where to spend your engineering effort. A system with a 1:1 read-write ratio might favor doing work at read time, since reads are not disproportionately expensive. A social feed with a 20:1 or 100:1 ratio should push as much work as possible to write time — precomputing, caching, denormalizing — because that cost is paid once per post, while a read-time cost is paid millions of times over for the same post.

04

High-Level Architecture and Components

With requirements and rough numbers in hand, we can now sketch the major pieces of the system. Think of this as the skeleton — later sections will go deep into how each piece actually works internally.

Client AppsMobile / Web API GatewayAuth · Rate limit · Routing Post Servicecreate + persist Feed Serviceassemble + read Kafka QueuePostCreatedEvent Fanout Servicepush / tag Graph Servicefollow lookups Ranking Servicescore & sort Notification Svcalerts Post StoreCassandra Graph StoreGraph DB / Redis Feed CacheRedis sorted sets Media / BlobS3-like CDNedge delivery Fig 4.1 — High-level architecture of the news feed system
Fig 4.1 — High-level architecture of the news feed system

Let us walk through each box and what it is responsible for. In an interview, being able to explain each component’s single job clearly is worth more than drawing a lot of boxes.

4.1 API Gateway

The single entry point for every client request. It handles authentication (is this a real, logged-in user?), rate limiting (is this user or IP sending too many requests?), and routing (which internal service should handle this particular request?). Production example: Netflix’s Zuul gateway and Amazon’s API Gateway both perform this exact role — a single, hardened front door so that internal services never have to deal directly with untrusted public traffic.

4.2 Post Service

Owns the lifecycle of a post: validating content, storing text/metadata in the post database, kicking off media uploads to blob storage, and — critically — publishing an event (“a new post was created”) onto a message queue rather than doing any fan-out work itself. Keeping this service simple and fast is important, because it is on the direct path of the user hitting “post,” and we do not want that click to hang while 100 million fan-out writes happen synchronously.

4.3 Follow Graph Service

Owns the “who follows whom” relationship — arguably the most important piece of state in the entire system, since almost everything else depends on knowing a user’s follower list and following list quickly. This is often backed by a graph database or, for very high read speed, a Redis-based sorted-set structure per user.

4.4 Fanout Service

Listens to the queue for “new post” events and decides how to distribute that post to followers’ feeds. This is where the celebrity problem is actually solved, using the hybrid model we will cover in detail shortly.

4.5 Ranking Service

Takes a raw, chronological set of candidate posts and re-orders them based on a relevance score — factoring in recency, the viewer’s past engagement with the poster, predicted likelihood of like/comment/share, and content type. We look at this in depth in its own section.

4.6 Feed Service

Handles the read path: when a user opens the app, this service assembles their feed, pulling from a precomputed cache where possible and merging in a live query for accounts that were not fanned-out (this is the “hybrid” part again — more shortly).

4.7 Notification Service

A supporting service that tells users “so-and-so just posted” or “your comment got a reply” — decoupled from the core feed pipeline via the same queue, so a slow notification delivery never blocks a post from reaching feeds.

4.8 Message Queue (Kafka)

The backbone that decouples “a post was created” from “that post reached everyone who needs to see it.” Production example: LinkedIn originally built Kafka precisely to handle this kind of high-throughput, decoupled event pipeline internally, before open-sourcing it — it is now the industry standard for exactly this pattern at companies including Netflix, Uber, and Airbnb.

💬
What an interviewer may ask

“Why put a message queue between the Post Service and the Fanout Service instead of having Post Service call Fanout Service directly?” Because a direct call would make post creation synchronously dependent on fan-out completing — and fan-out for a celebrity post can take much longer than a normal API call should. The queue lets Post Service respond to the user instantly (“your post is live”) while fan-out happens asynchronously in the background, and it also gives you natural retry and buffering behavior if the Fanout Service is temporarily overloaded or down.

05

Fanout-on-Write (Push Model)

Now let us go deep on the two fundamental strategies for getting a post from its author to its readers, starting with the one that optimizes reads at the cost of writes.

Imagine you keep a small mailbox for every single user — call it their feed cache — that already contains, pre-sorted, the IDs of posts they should see next. When someone posts, instead of waiting for a reader to ask “what is new,” the system immediately walks through every one of that author’s followers and drops the new post’s ID into each of their mailboxes. By the time any follower opens the app, their feed is already sitting there, ready to be read with a single fast cache lookup.

This is fan-out-on-write, also called the push model, because the work of distributing the post is “pushed out” at write time, immediately after the post is created.

Author Post Svc Kafka Fanout Svc Graph Svc Feed Cache Follower POST /post store in Post DB 201 Created (instant) publish PostCreated consume event get followers [F1,F2,…Fn] ZADD post_id for each follower (batched) GET /feed pre-built feed, instant Fig 5.1 — Fan-out-on-write: the post is pushed into every follower’s feed cache immediately
Fig 5.1 — Fan-out-on-write: the post is pushed into every follower’s feed cache immediately

The feed cache itself is usually modeled as a simple, capped-length sorted list per user, most naturally implemented with a Redis sorted set, where the score is the post’s timestamp (or a ranking score) and the value is the post ID.

FanoutWorker.java
// Fanout worker: pushes a new post into every follower's feed cache
// Uses Redis sorted sets — score = timestamp, member = postId

public class FanoutWorker {

    private static final int MAX_FEED_LENGTH = 1000; // cap per user
    private final RedisClient redis;
    private final FollowGraphClient graphClient;

    public void handlePostCreated(PostCreatedEvent event) {
        String authorId = event.getAuthorId();
        String postId = event.getPostId();
        long timestamp = event.getTimestamp();

        // Step 1: fetch follower list (paginated for very large accounts)
        Iterator<String> followerIds = graphClient.getFollowersStream(authorId);

        // Step 2: push post into each follower's feed, in batches
        List<String> batch = new ArrayList<>();
        while (followerIds.hasNext()) {
            batch.add(followerIds.next());
            if (batch.size() == 500) {
                pushBatch(batch, postId, timestamp);
                batch.clear();
            }
        }
        if (!batch.isEmpty()) {
            pushBatch(batch, postId, timestamp);
        }
    }

    private void pushBatch(List<String> followerIds, String postId, long timestamp) {
        try (Pipeline pipeline = redis.openPipeline()) {
            for (String followerId : followerIds) {
                String feedKey = "feed:" + followerId;
                pipeline.zadd(feedKey, timestamp, postId);
                // trim the feed so it never grows unbounded
                pipeline.zremrangeByRank(feedKey, 0, -MAX_FEED_LENGTH - 1);
            }
            pipeline.sync(); // executes all commands in one network round trip
        }
    }
}

5.1 Why this works so well for typical users

For an average account with a few hundred followers, fan-out-on-write is close to ideal. The write cost of a single post — a few hundred small, fast Redis operations — is trivial for the system, and it buys every follower an instant, pre-built feed read for the rest of that post’s lifetime. Since our capacity estimate showed a 20:1 (often much higher) read-to-write ratio, paying a small extra cost at write time to make every subsequent read nearly free is exactly the right trade for the vast majority of accounts on the platform.

5.2 Why it breaks down at the extreme

The problem is entirely about the tail of the distribution, not the average. A celebrity with 100 million followers turns one post into 100 million Redis writes. Even at a very efficient 50,000 writes per second per fan-out worker, that is roughly 2,000 seconds — over 30 minutes — just to fan out one single post, and that assumes nothing else is competing for the same workers at the same time. In practice, platforms run many parallel fan-out workers, but the point stands: the cost of fan-out-on-write scales directly with follower count, and follower count for a small number of accounts is genuinely enormous.

💬
What an interviewer may ask

“What happens to a normal follower’s feed while a celebrity’s fan-out is still in progress?” Nothing breaks — the follower simply will not see that specific post yet, and will typically see it appear within seconds to a couple of minutes as their shard of the fan-out completes. This is the eventual consistency trade-off we flagged in the requirements section: a short, invisible delay is an acceptable cost, in exchange for the system never falling over.

06

Fanout-on-Read (Pull Model)

The opposite strategy does no work at all when a post is created. The Post Service simply saves the post, and that is it — nothing gets copied anywhere. All the real work happens later, at the moment a follower actually opens their feed.

When that happens, the Feed Service looks up who the user follows, fetches the most recent posts from each of those accounts (typically from a cache, falling back to the database), merges them together, sorts by recency or relevance, and returns the result. This is fan-out-on-read, or the pull model, because the work of assembling the feed is “pulled” together on demand, at read time.

Reader Feed Svc Graph Svc Post Cache Post DB GET /feed who does this user follow? [A1, A2, … An] loop: for each followed account get recent posts (cache) cached posts (or miss) fetch on cache miss merge k sorted lists, rank assembled feed Fig 6.1 — Fan-out-on-read: nothing is precomputed, everything is assembled at read time
Fig 6.1 — Fan-out-on-read: nothing is precomputed, everything is assembled at read time

Notice the appeal immediately: posting is essentially free, no matter how many followers the author has. A celebrity with 100 million followers costs exactly the same at write time as a brand-new account with zero followers, because nothing gets copied to anyone. This completely sidesteps the celebrity problem — at write time.

But remember the 20:1 (or higher) read-to-write ratio we calculated earlier. The pull model takes a cost that used to happen once per post, and turns it into a cost that happens every single time any follower opens their feed. If a user follows 500 accounts, then every feed load means fetching recent posts from up to 500 different sources and merging them — repeated for every one of the hundreds of millions of daily feed opens. That is an enormous amount of repeated, redundant work, and it is precisely the wrong trade given how lopsided our read-write ratio is.

Where pull genuinely wins

The pull model is not just a fallback — it is the right default for accounts with enormous follower counts, low-activity users who barely open the app (precomputing a feed for someone who logs in once a month wastes effort), and for surfacing “possible” content like search results or explore pages where there is no fixed follow relationship to pre-fan-out along at all.

💬
What an interviewer may ask

“If pull avoids the celebrity problem so well, why not just use pull for everyone and skip fan-out-on-write entirely?” Because pull makes every feed read expensive, and reads vastly outnumber writes. Using pull for everyone would mean paying that expensive merge-and-rank cost hundreds of millions of times a day, for the vast majority of accounts where fan-out-on-write would have been nearly free. The right design uses each strategy where it is cheapest — which is exactly what the hybrid model does next.

07

The Celebrity Problem, Explained Deeply

Since this is the specific twist in our prompt, it deserves its own dedicated section rather than being folded into the fan-out discussion. Let us define it precisely and look at exactly why it is dangerous.

The celebrity problem (sometimes called the “hotspot” or “hot key” problem in distributed systems more broadly) happens whenever the amount of work a system must do for one entity is wildly disproportionate to the amount of work it does for a typical entity. In our case, the “entity” is a user account, and the “work” is fan-out writes. A normal account might trigger a few hundred writes per post. A celebrity account triggers tens of millions. That is not just “a bit more load” — it is several orders of magnitude more load, concentrated on one operation, at one moment in time.

This creates three concrete failure risks if left unhandled:

Risk 1

Fan-out latency spikes

A naive system that tries to fan out a celebrity’s post the same way it fans out anyone else’s post will take an extremely long time to finish — potentially many minutes — during which the fan-out workers are fully occupied and other, smaller posts queued behind it get delayed too. This is a classic head-of-line blocking problem: one giant job clogs the pipeline for everyone behind it.

Risk 2

Thundering herd on graph & cache

Reading “who are the 100 million followers of this account” and writing to 100 million different feed caches generates a massive, sudden burst of traffic against the Graph Service and Redis cluster. If those systems are not specifically designed to absorb bursty, extremely skewed traffic, this single event can degrade performance for completely unrelated users whose requests happen to land on the same overloaded shard.

Risk 3

Wasted work

A large fraction of a celebrity’s 100 million followers might not open the app for days, or might unfollow before ever seeing that particular post. Doing 100 million writes right now, for followers who might not read the result for a week (or ever), is wasted effort — the system paid full price at write time for a read that may never happen.

🏭
Production example — Twitter / X

Twitter engineers have written and spoken publicly for years about exactly this issue. Their solution, which is now the industry-standard pattern, is the hybrid model we cover next: fan-out-on-write for the vast majority of accounts, and fan-out-on-read specifically carved out for accounts above a follower threshold. Instagram and Facebook use conceptually similar hybrid strategies, tuned with their own thresholds and caching layers.

💬
What an interviewer may ask

“Could you just rate-limit how fast a celebrity can post, to reduce this problem?” That treats a symptom, not the cause — it degrades the product experience for high-value accounts (celebrities and brands are often key to platform engagement) and does not solve the underlying issue that fan-out cost scales with followers, not with posting frequency. A single post from a 100-million-follower account is still a 100-million-write problem, however rarely they post.

08

The Hybrid Push-Pull Model (The Actual Solution)

This is the section that answers our exact interview prompt. The industry-proven answer to “how do you handle celebrities with 100 million followers” is: do not use one strategy for everyone — pick the strategy per account, based on follower count, and merge the results at read time.

Here is the rule in plain language: pick a threshold — a common real-world number is somewhere between 10,000 and 1,000,000 followers, tuned based on actual system load testing. Any account below that threshold gets fan-out-on-write, because the write cost is cheap and it makes every future read instant. Any account above that threshold — the celebrities, the huge media pages — is treated with fan-out-on-read: their posts are never pushed anywhere, and instead get pulled in and merged live whenever a follower loads their feed.

New Post Createdauthor, post, timestamp Author’s followercount > threshold? Fan-out-on-Writepush post_id to everyfollower’s feed cache Skip fan-outmark as ‘celebrity post’store in Post DB + cache Follower opens feedFeed Service Follows anycelebrity? Fetch celebrity posts livemerge with cached feed Return cached feed directlyprecomputed by push No — normal user Yes — celebrity Yes No
Fig 8.1 — The hybrid model: threshold-based routing at write time, merge at read time

This design gives every account the cheapest possible path. A normal user’s post costs a few hundred writes and gives every follower an instant read. A celebrity’s post costs almost nothing at write time — it is simply saved — and the small extra cost of fetching celebrity content is paid only by the users who actually follow celebrities, and only at the moment they actually check their feed, not by the celebrity’s followers who never open the app that day.

FanoutRouter.java
// Deciding fanout strategy at write time
public class FanoutRouter {

    private static final long CELEBRITY_THRESHOLD = 1_000_000L;
    private final FollowGraphClient graphClient;
    private final FanoutWorker fanoutWorker;
    private final PostStore postStore;

    public void handleNewPost(Post post) {
        long followerCount = graphClient.getFollowerCount(post.getAuthorId());

        if (followerCount > CELEBRITY_THRESHOLD) {
            // Celebrity path: no fanout, just mark and store.
            // We tag the author as a "celebrity" so the read path
            // knows to fetch their posts live instead of expecting
            // them in the follower's precomputed feed cache.
            postStore.save(post);
            postStore.markCelebrityPost(post.getAuthorId(), post.getPostId());
        } else {
            // Normal path: eagerly push to every follower's feed.
            postStore.save(post);
            fanoutWorker.handlePostCreated(
                new PostCreatedEvent(post.getAuthorId(), post.getPostId(), post.getTimestamp())
            );
        }
    }
}

// Assembling a feed at read time — the merge step
public class FeedAssembler {

    private final RedisClient feedCache;
    private final FollowGraphClient graphClient;
    private final PostStore postStore;

    public List<Post> getFeed(String userId, int limit) {
        // Step 1: get the precomputed feed (built by fanout-on-write)
        List<String> cachedPostIds = feedCache.zrevrange("feed:" + userId, 0, limit);

        // Step 2: find celebrities this user follows
        List<String> celebrityIds = graphClient.getFollowedCelebrities(userId);

        // Step 3: pull recent posts from those celebrities live
        List<Post> celebrityPosts = new ArrayList<>();
        for (String celebId : celebrityIds) {
            celebrityPosts.addAll(postStore.getRecentPosts(celebId, 20));
        }

        // Step 4: merge cached feed with live celebrity posts, then rank
        List<Post> cachedPosts = postStore.getPostsByIds(cachedPostIds);
        List<Post> merged = mergeAndDedupe(cachedPosts, celebrityPosts);
        return RankingEngine.rank(userId, merged, limit);
    }

    private List<Post> mergeAndDedupe(List<Post> a, List<Post> b) {
        Map<String, Post> byId = new LinkedHashMap<>();
        for (Post p : a) byId.put(p.getPostId(), p);
        for (Post p : b) byId.put(p.getPostId(), p);
        return new ArrayList<>(byId.values());
    }
}

8.1 Handling the boundary: what if a normal account suddenly goes viral?

A subtle real-world wrinkle: follower counts change, sometimes very quickly. An account with 50,000 followers today could gain a million overnight after going viral. Production systems handle this with a periodic or event-triggered re-evaluation: whenever a follower count crosses the threshold, a background job flips that account’s fanout mode, and — importantly — this switch does not require any migration of existing feed data, because the switch only changes how future posts are distributed. Old, already-fanned-out posts remain in followers’ cached feeds; the account simply starts being treated as a celebrity going forward.

8.2 Handling the boundary: what if a celebrity has 100 million followers but a user follows only 3 celebrities?

This is exactly why the merge step in the read path is cheap. The Feed Service does not fetch anything from all 100 million followers — it only does a small amount of extra work, per reader, proportional to how many celebrities that specific reader follows (usually a handful). The expensive part of the celebrity’s fan-out — the “100 million” — never actually happens anywhere in this design. It is replaced by many small, cheap lookups, each done only when a real reader actually needs it.

💬
What an interviewer may ask

“How would you pick the actual follower-count threshold?” There is no universal magic number — the right approach is to say you would determine it empirically: load-test the fan-out pipeline to find the follower count at which fan-out latency or resource cost crosses an unacceptable line, and set the threshold safely below that, then monitor and adjust it over time as infrastructure capacity changes. Showing that you would measure rather than guess is exactly the signal interviewers are listening for.

09

Data Flow and Lifecycle of a Post

It helps to trace a single post from the moment it is created to the moment it disappears from active circulation, because this end-to-end view is often exactly what interviewers want you to narrate.

StageWhat happens
1 — CreationA user writes a caption, attaches a photo, and taps “post.” The client uploads the media directly to blob storage (getting back a URL), then sends the post metadata — text, media URL, timestamp, author ID — to the Post Service through the API Gateway.
2 — PersistenceThe Post Service validates the content (length limits, banned words, spam heuristics), writes a row into the sharded post database, and immediately returns a success response to the client. From the user’s point of view, posting is done — everything after this point happens invisibly, in the background.
3 — Event publicationThe Post Service publishes a “PostCreated” event onto a Kafka topic, carrying the author ID, post ID, and timestamp. This decouples “the post exists” from “the post has been distributed,” which is the key insight that keeps posting fast regardless of what happens downstream.
4 — Fanout decisionThe Fanout Service consumes the event and checks the author’s follower count against the celebrity threshold, choosing push or pull-tagging accordingly.
5 — DistributionFor a push-model author, the post ID lands in every follower’s Redis feed list within seconds. For a pull-model (celebrity) author, nothing moves yet — the post simply waits in the Post DB and post cache to be picked up on demand.
6 — ConsumptionA follower opens the app. The Feed Service reads the precomputed feed, merges in any followed celebrities’ recent posts, ranks the combined set, hydrates each post ID into full post content (text, media URLs, like counts), and returns it to the client, which renders it and lazily loads images from the CDN as the user scrolls.
7 — Interaction loopAs the user likes, comments, or lingers on posts, those signals are sent back asynchronously to an engagement-tracking pipeline, which feeds the Ranking Service’s models — closing the loop so future feeds get better personalized over time.
8 — Aging outFeed caches are capped in length (roughly 1,000 entries per user) and posts eventually age out of active caches, falling back to being retrievable only from the primary post database if a user scrolls far enough back — a classic hot/cold storage split that keeps the expensive cache tier small and fast.
💬
What an interviewer may ask

“At what point in this lifecycle would a user be told ‘your post is live’ — and does that matter?” It matters a lot. The response should be sent right after Stage 2 (persistence), not after Stage 5 (distribution) completes. Tying the user-facing response to full distribution would mean a celebrity’s “post successful” message could take much longer to appear than a normal user’s, which is exactly the kind of user-visible inconsistency good asynchronous design is meant to avoid.

10

Data Model and Storage Schema

Let us define the core entities and how they relate to each other, then look at concrete schema choices.

USER user_id (PK)usernamefollower_countis_celebritycreated_at FOLLOW follower_id (FK)followee_id (FK)followed_at POST post_id (PK)author_id (FK)text_contentcreated_atlike_countcomment_count ENGAGEMENT engagement_id (PK)post_id (FK)user_id (FK)type MEDIA media_id (PK)post_id (FK)url, type follows (as follower) creates receives contains
Fig 10.1 — Core entity relationships

A few schema design decisions are worth calling out explicitly, because they are exactly the kind of thing interviewers probe on.

10.1 Why the Post table is partitioned by author_id, not post_id

It is tempting to shard posts by a random or hash-based post ID for even distribution, but the most common access pattern is “give me this author’s recent posts” (needed constantly by the pull-model celebrity path, and by profile pages). Partitioning primarily by author_id keeps one author’s posts physically close together, making that very common query cheap — a single shard lookup instead of a scatter-gather across many shards.

10.2 Why post IDs should be time-sortable, not purely random

Using a scheme like Snowflake IDs (popularized by Twitter, later adopted in similar forms by Instagram and Discord) bakes a timestamp into the most significant bits of the ID itself. This means sorting posts by ID is the same as sorting them by creation time, without needing a separate index or an extra sort step — a small trick that saves enormous amounts of computation at this scale.

SnowflakeIdGenerator.java
// Simplified Snowflake-style unique ID generator
// 41 bits timestamp | 10 bits machine/shard id | 12 bits sequence number
public class SnowflakeIdGenerator {

    private static final long EPOCH = 1700000000000L; // custom epoch
    private final long machineId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public SnowflakeIdGenerator(long machineId) {
        this.machineId = machineId;
    }

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis() - EPOCH;

        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 0xFFF; // 12 bits, wraps at 4096
            if (sequence == 0) {
                // sequence exhausted for this millisecond, wait for next one
                while (timestamp == lastTimestamp) {
                    timestamp = System.currentTimeMillis() - EPOCH;
                }
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;

        return (timestamp << 22) | (machineId << 12) | sequence;
    }
}

10.3 Why the Follow relationship needs two access patterns, not one

Almost every operation in this system needs to answer one of two very different questions: “who does this user follow?” (needed to build a feed) and “who follows this user?” (needed to fan out a post). Storing the relationship once and querying it two directions is inefficient at scale, so production systems typically maintain two denormalized copies of the same relationship — a “following list” keyed by follower, and a “followers list” keyed by followee — accepting the small extra write cost and storage duplication in exchange for both reads being equally fast.

💬
What an interviewer may ask

“Isn’t storing the follow relationship twice a normalization violation?” Yes, technically — and that is fine. This is a deliberate, common trade-off in large-scale systems called denormalization: you accept some data duplication and the extra complexity of keeping both copies in sync, in exchange for read performance that a fully normalized model could not give you at this traffic level. Recognizing when to break normalization rules on purpose is a strong signal of practical, production-level experience.

11

Databases Deep Dive: Picking the Right Store for Each Job

A common interview trap is treating “the database” as one monolithic decision. A real system like this uses several different storage technologies, each chosen for a specific access pattern.

DataStore typeWhyProduction example
Post metadataWide-column NoSQL (Cassandra / ScyllaDB)Extremely high write throughput, partition by author_id, tunable consistencyInstagram’s original feed metadata store; Facebook’s TAO layer
Follow graphRedis sets / dedicated graph storeMillisecond-speed membership and list lookups at massive scaleTwitter’s FlockDB (purpose-built for social edges)
Precomputed feedsRedis sorted sets (in-memory)Sub-millisecond reads, natural sorted structure for ranked listsTwitter’s original Redis-based “Timeline Service”
Media (photos/video)Object/blob storage (S3-style)Cheap, durable, virtually unlimited storage for large binary filesAmazon S3 underpins media for a huge share of the internet
User/profile dataRelational DB (sharded MySQL/Postgres)Strong consistency needed for account, billing, and settings dataFacebook’s sharded MySQL fleet
Engagement eventsAppend-only log (Kafka) + analytics storeHigh write volume, sequential access, feeds ML pipelinesLinkedIn’s Kafka-based activity stream

11.1 SQL vs NoSQL for post storage — the real reasoning

A relational database gives you strong consistency and rich query flexibility (joins, transactions), which is valuable for data like account settings or payments where correctness matters more than raw throughput. But a single relational database, even a powerful one, struggles to sustain tens of thousands of writes per second while also serving low-latency reads, because relational engines pay overhead for maintaining strict consistency guarantees and complex indexes.

A wide-column store like Cassandra takes a different trade: it is built from the ground up for horizontal scale and high write throughput, using a partition-and-replicate model where you deliberately give up some consistency guarantees (you typically read “eventually consistent” data unless you explicitly ask for stronger consistency at extra cost) in exchange for near-linear scalability — write throughput that grows almost proportionally as you add machines. For post metadata, where a few seconds of eventual consistency is completely acceptable (as we established in the requirements), this trade is clearly worth it.

💡
A concrete comparison

Picture Cassandra’s ring-based architecture: data is distributed across nodes using consistent hashing (which we cover in the sharding section), and each piece of data is replicated to multiple nodes. A write succeeds once a configurable number of replicas acknowledge it — this is the tunable consistency level. Setting it to a lower number (say, 1 out of 3 replicas) gives blazing write speed at the cost of a small window where reads might not immediately reflect the latest write; setting it higher gives stronger consistency at some latency cost. For a news feed, favoring speed with a low consistency level is almost always the right call.

💬
What an interviewer may ask

“Why not just use Postgres or MySQL for everything, given that they are mature and well understood?” A fair follow-up would be to acknowledge that a single, well-sharded relational database absolutely can handle significant scale — many real systems do. But once write throughput and horizontal scale requirements grow past what manual sharding of a relational database can comfortably handle, a purpose-built distributed store designed for exactly this workload (like Cassandra) removes a lot of the operational complexity you would otherwise have to build yourself on top of a relational engine.

12

Caching Strategy

Given our 20:1-or-higher read-to-write ratio, caching is not an optimization we bolt on later — it is central to the whole design. Let us cover the layers of caching this system needs, and a genuinely tricky problem specific to celebrity accounts: the hot key problem.

12.1 Layer 1 — Precomputed feed cache

Already covered: a Redis sorted set per user, holding the last ~1,000 post IDs for their feed, populated by fan-out-on-write. This is the single most important cache in the system, since it turns the most frequent operation (loading a feed) into a fast in-memory lookup.

12.2 Layer 2 — Post content cache

Feed caches store only post IDs, not full content (to keep them small and cheap to update). A separate cache — typically also Redis, or a dedicated in-memory content store — maps post IDs to their full hydrated content: text, media URLs, current like/comment counts. This is populated with a standard cache-aside pattern: check the cache first, and on a miss, fetch from the Post DB and populate the cache for next time.

12.3 Layer 3 — Follow graph cache

Follower and following lists are read extremely frequently (every fan-out, every feed merge) and change relatively rarely compared to how often they are read, making them an excellent caching candidate — often held almost entirely in Redis rather than falling back to a slower backing store except for cold, rarely-accessed accounts.

12.4 The hot key problem — when one cache key gets too much traffic

Here is a genuinely subtle issue that trips up a lot of otherwise strong designs. A celebrity’s post content, once hydrated, might be requested millions of times in a short window — every follower who happens to check their feed around the same time asks the content cache for the same post ID. In a typical distributed cache, that key lives on one specific cache node (determined by its hash). If millions of requests hit that same node for that same key at once, that single node can become overwhelmed, even though the overall cache cluster has plenty of spare capacity elsewhere. This is exactly what people mean by a “hot key” or “hotspot” — the problem is not total load, it is load concentrated unevenly on one small part of the system.

HotKeyAwareCache.java
// A simple local "L1" cache in front of the distributed cache,
// specifically to absorb hot-key traffic for viral / celebrity posts.
public class HotKeyAwareCache {

    private final Cache<String, Post> localCache; // small, in-process, short TTL
    private final RedisClient distributedCache;    // the normal shared L2 cache

    public HotKeyAwareCache() {
        this.localCache = Caffeine.newBuilder()
            .maximumSize(5_000)
            .expireAfterWrite(Duration.ofSeconds(5)) // short TTL is enough to smooth spikes
            .build();
    }

    public Post getPost(String postId) {
        // Check the tiny local cache first — this alone can absorb
        // a huge share of traffic for a viral post, because thousands
        // of requests on the same application server all hit the same
        // in-process cache instead of all hammering Redis.
        Post cached = localCache.getIfPresent(postId);
        if (cached != null) {
            return cached;
        }

        Post post = distributedCache.get("post:" + postId);
        if (post == null) {
            post = postStore.getById(postId); // fall back to DB on full miss
            distributedCache.set("post:" + postId, post, Duration.ofMinutes(10));
        }
        localCache.put(postId, post);
        return post;
    }
}

The trick above — a small, short-lived local cache sitting in front of the shared distributed cache — is a genuinely common production pattern, because it converts a single hot key hitting one Redis node millions of times into that same key being served locally, on every individual application server, most of the time. It is cheap to implement and dramatically reduces the blast radius of any one piece of content going viral.

12.5 Cache invalidation for changing data

Like counts and comment counts change constantly on a popular post, which conflicts with wanting to cache that same post’s content for a while. The common resolution is to split the data: cache the mostly-static content (text, media URLs) with a longer TTL, and serve frequently-changing counters from a separate, very fast counter store (often Redis INCR-based counters) that is read alongside the cached content and merged at render time, rather than invalidating the whole cached post every time someone likes it.

💬
What an interviewer may ask

“How would you detect a hot key before it causes an outage, rather than after?” By monitoring per-key access frequency at the cache layer (many caching systems and proxies, like Twemproxy or a custom sidecar, can sample and report top-N accessed keys), and by watching for uneven CPU or network load across otherwise-identical cache nodes — a classic sign that one node is disproportionately serving a hot key.

13

Sharding and Load Balancing

With 500 million users and 250 million daily posts, no single machine — no matter how powerful — could hold all the data or handle all the traffic. The system has to be split across many machines, a process called sharding or partitioning, and traffic has to be distributed evenly across those machines, which is load balancing.

13.1 Choosing a partition key

The most natural partition key for both the post store and the feed cache is user ID (author_id for posts, follower’s user_id for feed caches) — because almost every query in this system is scoped to a specific user, either “posts by this author” or “feed for this reader.” Partitioning by user ID keeps those queries confined to a single shard, avoiding expensive scatter-gather queries across the whole cluster.

13.2 Consistent hashing — why it beats simple modulo sharding

A naive approach — shard = hash(userId) % numberOfShards — works until you need to add or remove a shard, at which point nearly every key remaps to a different shard, forcing a massive, disruptive data migration. Consistent hashing solves this by arranging shards on a conceptual ring, so that adding or removing one shard only reshuffles the small slice of keys immediately next to it on the ring, leaving the vast majority of keys undisturbed.

Shard A Shard B Shard C Shard D user_789 user_456 user_123 Fig 13.1 — Keys map to the nearest shard clockwise on the hash ring
Fig 13.1 — Keys map to the nearest shard clockwise on the hash ring
ConsistentHashRing.java
// Simplified consistent hashing ring using virtual nodes
// Virtual nodes smooth out uneven load distribution across physical shards
public class ConsistentHashRing {

    private final TreeMap<Long, String> ring = new TreeMap<>();
    private static final int VIRTUAL_NODES_PER_SHARD = 100;

    public void addShard(String shardId) {
        for (int i = 0; i < VIRTUAL_NODES_PER_SHARD; i++) {
            long hash = hash(shardId + "#" + i);
            ring.put(hash, shardId);
        }
    }

    public void removeShard(String shardId) {
        for (int i = 0; i < VIRTUAL_NODES_PER_SHARD; i++) {
            ring.remove(hash(shardId + "#" + i));
        }
    }

    public String getShardFor(String key) {
        long hash = hash(key);
        Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
        if (entry == null) {
            entry = ring.firstEntry(); // wrap around the ring
        }
        return entry.getValue();
    }

    private long hash(String input) {
        return Hashing.murmur3_128().hashString(input, StandardCharsets.UTF_8).asLong();
    }
}

Virtual nodes, shown in the code above, solve a secondary problem: with only a handful of real shards placed on the ring, load can still end up quite uneven if two shards happen to land close together. Giving each physical shard 100 (or more) virtual positions on the ring spreads its share of the keyspace out evenly, so real-world load balances much more smoothly across physical machines.

13.3 Load balancing at the service layer

Beyond data sharding, stateless services (API Gateway, Feed Service, Fanout Service) sit behind standard load balancers that distribute incoming requests across many identical service instances, usually with health checks so traffic automatically avoids instances that are unhealthy or overloaded. Production example: Netflix’s internal services sit behind their own load balancing layer, Eureka-based service discovery historically, now often paired with more modern service mesh technology, so that any instance can be added or removed without manual reconfiguration.

💬
What an interviewer may ask

“What happens to a celebrity’s data specifically under this sharding scheme — does not their shard become a hotspot?” Yes, exactly — this is the same hot key problem from the caching section, showing up again at the storage layer. The common fix is to detect exceptionally large accounts and give them dedicated, isolated shards or additional read replicas specifically for their data, rather than letting them share a shard sized for average-case load.

13.4 Rebalancing without downtime

Adding a new shard to a running cluster is a routine, ongoing operational task at this scale, not a rare event — user growth and data growth both push clusters toward needing more capacity over time. Consistent hashing already limits how much data has to move when a shard is added, but that data still has to move somewhere, and it has to do so without interrupting live traffic. Production systems typically handle this with a background migration process: the new shard is added to the ring, and a controlled, throttled process copies the specific slice of keys that now belong to it from their old shard, while both the old and new locations remain readable during the transition. Only once the copy is verified complete does the system fully cut over reads and writes for that slice of keys to the new shard, and the old copy is cleaned up afterward. Throttling this migration matters just as much as doing it correctly — an unthrottled bulk copy competing with live production traffic for the same disk and network resources can itself become a self-inflicted outage, which is a subtlety many first-draft designs miss entirely.

13.5 Read replicas versus shards — a distinction worth keeping straight

It is easy to conflate two related but different scaling techniques. Sharding splits the entire dataset into disjoint pieces, each living on a different set of machines, so that no single machine needs to hold all the data. Read replication, by contrast, copies the same piece of data onto multiple machines specifically so that read traffic against that data can be spread across more than one machine. A well-scaled system typically uses both together: a partition key decides which shard owns a given piece of data, and then that shard itself has several read replicas behind it, so that both the total dataset size and the read throughput against any individual slice of that dataset can each grow independently, along their own dimension, as the platform’s usage patterns demand.

14

Feed Ranking: From Chronological to Personalized

Everything so far has assembled a candidate set of posts — the raw material for a feed. Ranking decides the order those posts actually appear in, and it is arguably the single biggest lever platforms have over how engaging a feed feels.

14.1 Why not just sort by time?

A purely chronological feed is simple, predictable, and easy to reason about — and for years it is exactly what most platforms used. But it has an obvious flaw: if you follow 500 accounts and only check your feed twice a day, a strictly time-sorted feed means the handful of accounts that happen to post most frequently completely dominate what you see, burying content from accounts you care about more but who post less often. Purely chronological ordering treats “most recent” as identical to “most relevant,” and those two things are often quite different.

14.2 Moving to relevance-based ranking

Modern feeds score each candidate post with a model that combines several signals, then sort by that score instead of raw timestamp. A simplified version of the kinds of features involved:

Signal 1

Recency

Newer posts generally score higher, but with decay rather than a hard cutoff.

Signal 2

Affinity

How much has this viewer historically engaged with this specific author (likes, comments, profile visits)?

Signal 3

Content-type fit

Does this viewer tend to engage more with video, photos, or text posts?

Signal 4

Predicted engagement

A machine learning model’s estimate of how likely this specific viewer is to like, comment on, or share this specific post.

Signal 5

Post velocity

How quickly is this post already accumulating engagement from others (an early social-proof signal)?

RankingEngine.java
// Simplified relevance scoring — real systems replace the
// hand-tuned weights below with a trained ML model, but the
// feature set and overall shape stays conceptually similar.
public class RankingEngine {

    public static List<Post> rank(String viewerId, List<Post> candidates, int limit) {
        PriorityQueue<ScoredPost> heap = new PriorityQueue<>(
            Comparator.comparingDouble(ScoredPost::score).reversed()
        );

        for (Post post : candidates) {
            double score = computeScore(viewerId, post);
            heap.add(new ScoredPost(post, score));
        }

        List<Post> result = new ArrayList<>();
        while (!heap.isEmpty() && result.size() < limit) {
            result.add(heap.poll().post());
        }
        return result;
    }

    private static double computeScore(String viewerId, Post post) {
        double recencyScore = decay(post.getTimestamp());
        double affinityScore = AffinityStore.get(viewerId, post.getAuthorId());
        double engagementScore = EngagementPredictor.predict(viewerId, post);
        double velocityScore = Math.log(1 + post.getRecentLikeVelocity());

        // Weighted combination — weights are typically learned,
        // not hand-picked, in a real production ranking model.
        return (0.25 * recencyScore)
             + (0.30 * affinityScore)
             + (0.35 * engagementScore)
             + (0.10 * velocityScore);
    }

    private static double decay(long timestamp) {
        long ageSeconds = (System.currentTimeMillis() / 1000) - timestamp;
        return Math.exp(-ageSeconds / 43200.0); // half-life-style decay, ~12 hours
    }
}
🏭
Production example — Facebook & Instagram

Facebook’s News Feed ranking has evolved through many generations of machine learning models, moving from simple heuristics in the early years to deep learning models that weigh thousands of signals per post per viewer. Instagram made a well-publicized shift away from strict chronological ordering years ago specifically because engagement and time-spent metrics improved substantially once ranking accounted for affinity and predicted interest rather than only recency.

14.3 The exploration vs exploitation trade-off

A purely engagement-maximizing ranker risks over-fitting to a viewer’s past behavior, showing them an increasingly narrow slice of content and starving new or different creators of visibility (a real concern often described as a filter bubble effect). Production ranking systems typically inject some deliberate exploration — occasionally surfacing content slightly outside a viewer’s established pattern — both to keep the feed feeling fresh and to keep gathering the training signal needed to keep the underlying prediction models accurate over time.

💬
What an interviewer may ask

“Where does ranking happen — should it run inside the Feed Service, or as a separate service?” Separating it into its own Ranking Service is usually the better answer: ranking models get retrained and redeployed frequently and independently of the rest of the feed pipeline, they often need specialized hardware (GPUs for larger models), and isolating them limits the blast radius if a ranking model misbehaves or needs a rollback — the Feed Service can fall back to simple chronological order if the Ranking Service is temporarily unavailable, rather than the whole feed breaking.

15

APIs and Microservices Design

Let us define the actual contracts a client application would use, and talk about why this system is naturally suited to a microservices architecture rather than one large application.

15.1 Core API endpoints

EndpointMethodPurpose
/v1/postsPOSTCreate a new post
/v1/posts/{postId}GETFetch a single post’s full content
/v1/feedGETFetch the caller’s personalized, ranked feed (paginated with a cursor)
/v1/users/{userId}/followPOSTFollow a user
/v1/users/{userId}/unfollowPOSTUnfollow a user
/v1/posts/{postId}/likePOSTLike a post (feeds the ranking engagement signal)
feed.http
GET /v1/feed?cursor=eyJ0cyI6MTcwMDAwMDAwMH0&limit=20 HTTP/1.1
Authorization: Bearer <token>

Response 200 OK:
{
  "posts": [
    {
      "postId": "1823456789012345",
      "authorId": "u_9182",
      "text": "Excited to share our new feature launch!",
      "mediaUrls": ["https://cdn.example.com/media/abcd.jpg"],
      "likeCount": 4210,
      "commentCount": 312,
      "createdAt": "2026-07-24T09:14:00Z"
    }
  ],
  "nextCursor": "eyJ0cyI6MTY5OTk5OTk4MH0"
}

Note the use of a cursor rather than a page number for pagination. Feeds are constantly changing — new posts arrive continuously — so offset-based pagination (“page 2, page 3”) tends to produce duplicate or skipped posts as the underlying data shifts between requests. A cursor, typically encoding the timestamp or rank of the last item seen, gives a stable, consistent scroll experience regardless of what is being written concurrently.

15.2 Why microservices fit this problem well

Post creation, fan-out, ranking, and feed assembly have genuinely different scaling characteristics and failure profiles. The Fanout Service needs to burst-scale during celebrity posting events. The Ranking Service needs specialized compute and frequent independent deployment. The Feed Service needs to stay responsive above almost everything else, since it is the most latency-sensitive read path in the whole product. Splitting these into independently deployable, independently scalable services means a spike or an incident in one does not automatically take down the others — a monolith handling all of this in one process would couple failure domains together unnecessarily.

The other side of microservices

Splitting into services is not free. It introduces network calls where there used to be function calls, adds operational complexity (more things to deploy, monitor, and version), and makes certain kinds of debugging — tracing a request across five services — meaningfully harder without good tooling. A reasonable interview answer acknowledges this trade-off explicitly rather than treating microservices as an unambiguous win.

💬
What an interviewer may ask

“How would you handle a partial failure — say, the Ranking Service is down when a user requests their feed?” Design for graceful degradation: the Feed Service should catch the failure and fall back to a simpler ordering (recency-based, or the last successfully cached ranked order) rather than failing the whole request. Users seeing a slightly less personalized feed is a far better outcome than users seeing an error page.

16

Design Patterns and Anti-Patterns

16.1 Patterns worth naming explicitly

Pattern

Event-driven architecture

The Post Service publishes events rather than calling downstream services directly, which is what let us decouple posting speed from fan-out speed throughout this design.

Pattern

CQRS

Command Query Responsibility Segregation — the “write path” (creating a post, fanning it out) and the “read path” (assembling and ranking a feed) are handled by entirely separate services with separate data representations optimized for each job, rather than one shared code path trying to serve both well.

Pattern

Cache-aside

Used throughout for post content and follow-graph lookups: check cache, fall back to source of truth on a miss, populate cache for next time.

Pattern

Circuit breaker

Services calling the Ranking Service or Graph Service should wrap those calls in a circuit breaker that “trips” and fails fast (falling back to a default behavior) if the downstream service is unhealthy, rather than piling up slow, doomed requests that make the outage worse.

Pattern

Bulkhead isolation

Giving celebrity accounts dedicated resources (as discussed in sharding) is a form of bulkheading: isolating a known risk so it cannot sink the whole ship.

16.2 Anti-patterns to avoid — and explicitly call out in an interview

Anti-pattern — Synchronous fan-out on the write path

Making the user’s “post” request wait for fan-out to fully complete before responding is the single most common mistake in a first-draft design — it directly reintroduces the celebrity latency problem right where it hurts most: the user-visible response time.

Anti-pattern — Treating all accounts identically

A one-size-fits-all fan-out strategy, as we have covered at length, either wastes resources for small accounts (over-provisioning for a case that rarely happens) or collapses under celebrity load.

Anti-pattern — Unbounded feed cache growth

Never capping how many post IDs accumulate in a user’s feed cache eventually turns a cheap in-memory structure into a memory and latency problem of its own — always cap and trim, as shown in the earlier fan-out code.

Anti-pattern — Ignoring idempotency

If a fan-out worker crashes partway through processing a celebrity’s followers and the message is retried, followers who were already processed should not get duplicate feed entries. Using idempotent operations (like Redis’s ZADD, which simply updates a score rather than creating duplicates for the same member) avoids this trap.

💬
What an interviewer may ask

“Give an example of how you would make the fan-out pipeline idempotent end-to-end, not just at the Redis level.” Assign each PostCreatedEvent a unique event ID, and have the Fanout Service track which event IDs it has already fully processed (e.g., in a short-lived dedupe table or cache). If the same event is redelivered — a normal occurrence in most message queue systems, which typically guarantee at-least-once delivery rather than exactly-once — the worker can detect it is already been handled and skip redundant work safely.

17

Advantages, Disadvantages, and Trade-offs

No design in distributed systems is free of trade-offs — the mark of a mature engineer is being able to state them plainly instead of presenting a design as if it has no downsides.

StrategyAdvantagesDisadvantages
Fan-out-on-write (push)Near-instant reads; simple read path; predictable read latencyExpensive, slow writes for high-follower accounts; wasted work for inactive followers; storage overhead of duplicated feed data
Fan-out-on-read (pull)Cheap, constant-time writes regardless of follower count; no wasted work for inactive followersExpensive reads, especially for users following many accounts; higher read latency; more load on shared post stores
Hybrid (this design)Cheap writes for large accounts, cheap reads for typical accounts; scales gracefully across the whole follower distributionMore implementation complexity; requires threshold tuning and monitoring; two code paths to maintain and test

The overall system-level trade-off worth stating explicitly in an interview: this design deliberately favors availability and low latency over strict consistency almost everywhere. A follower briefly not seeing a new post, a like count that is a few seconds stale, a feed that is slightly less personalized during a partial outage — all of these are accepted costs, in exchange for a system that stays fast and up even under heavy, uneven load. That is exactly the right trade for a social feed, where a few seconds of staleness is invisible to users but even a brief full outage is highly visible and damaging.

💬
What an interviewer may ask

“If you had to give up either strong consistency or high availability for this specific product, which would you choose, and why?” Availability, without much hesitation — a news feed is fundamentally a browsing experience, not a transactional one. Nobody’s financial balance or safety depends on seeing a post the exact millisecond it is created; the cost of a short delay is essentially zero, while the cost of the whole feed being unreachable is a serious, visible product failure.

18

Performance and Scalability

Let us connect the earlier capacity numbers to concrete scaling techniques.

18.1 Horizontal scaling as the primary lever

Every stateful component in this design — post storage, follow graph, feed cache — is partitioned so that adding more machines directly increases capacity, rather than relying on making any single machine bigger (vertical scaling), which always hits a hard ceiling. This is what makes it possible to grow from 500 million to, hypothetically, a billion daily active users largely by adding shards and cache nodes rather than redesigning the system.

18.2 Read replicas for further read scaling

Beyond sharding, each shard typically has multiple read replicas — copies of the data that can serve read traffic while a single primary handles writes. Since our read-to-write ratio is so heavily skewed toward reads, having several read replicas per shard lets read capacity scale independently of write capacity, which is exactly where the imbalance in our capacity estimation needs the most help.

18.3 Batching and pipelining

The fan-out code shown earlier deliberately batches Redis writes and uses pipelining (sending many commands in one network round trip rather than one at a time). At the scale of millions of writes for a single celebrity post, this single technique can be the difference between a fan-out finishing in minutes versus hours, because network round-trip latency, not actual processing time, dominates cost when operations are small and numerous.

18.4 Backpressure and queue-based load shedding

During an extreme spike — say, several large accounts posting within the same minute — the Fanout Service’s consumers might not keep up in real time with the incoming event rate. Because the Post Service publishes events to a durable queue rather than calling Fanout Service directly, this is safe: events simply queue up and get processed as capacity allows, rather than being lost or causing cascading failure. This buffering behavior — sometimes called backpressure absorption — is one of the most valuable properties a message queue provides in bursty systems like this one.

18.5 Precomputing versus computing on demand — a recurring theme

Across nearly every part of this design — feed caches, denormalized follow lists, cached post content — the same underlying idea repeats: shift work from the frequent operation (read) to the infrequent one (write), whenever the read-write ratio justifies it. Recognizing this as a repeating pattern, rather than a series of unrelated tricks, is exactly the kind of systems-level thinking interviewers are trying to draw out.

💬
What an interviewer may ask

“Suppose read traffic grows 10x but write traffic stays flat — what would you change?” Add more read replicas and cache capacity, since the imbalance the system was already designed around simply gets more extreme rather than fundamentally different. The architecture does not need to change shape — it needs more of the same kind of horizontal capacity on the read-heavy tiers specifically, which is exactly why partitioning reads and writes into independently scalable tiers from the start pays off.

19

High Availability, Reliability, and the CAP Theorem

19.1 A quick, precise refresher on the CAP theorem

The CAP theorem states that a distributed system can only fully guarantee two of these three properties at once, during a network partition (when some machines cannot communicate with others):

C

Consistency

Every read receives the most recent write, or an error.

A

Availability

Every request receives a response, even if it might not reflect the latest write.

P

Partition tolerance

The system continues operating despite network partitions between nodes.

In practice, partition tolerance is not really optional for any real distributed system spread across multiple machines or data centers — networks will experience partitions eventually, full stop. So the real, practical choice most systems actually make is between CP (favor consistency, sacrifice availability during a partition) and AP (favor availability, sacrifice strict consistency during a partition).

As established throughout this tutorial, a news feed system should lean heavily AP. Users tolerate a few seconds of staleness far better than they tolerate an error page. Cassandra (used for our post store) and Redis clusters (used for our caches) are both commonly configured to favor availability, with tunable consistency that we deliberately dial toward “fast and eventually correct” rather than “always perfectly correct, sometimes unavailable.”

19.2 Replication for durability and availability

Every piece of data in this system is replicated across multiple nodes — commonly three or more copies, often spread across different physical availability zones or data centers. If one node or even an entire data center fails, replicas elsewhere can continue serving traffic. Cassandra achieves this with a configurable replication factor and a “quorum” style read/write model: writes and reads can require acknowledgment from a subset of replicas (say 2 out of 3), balancing consistency and availability per operation.

19.3 Consensus — where it is actually needed, and where it is not

It is worth being precise here, because a common interview mistake is reaching for heavyweight consensus algorithms (like Raft or Paxos) everywhere, when most of this system does not need them. Consensus protocols exist to make multiple nodes agree on a single, strictly ordered sequence of operations — essential for things like leader election in a database cluster, or maintaining strong consistency for critical configuration data. But the bulk of this design — feed caches, post content, engagement counters — deliberately uses weaker, eventually-consistent replication specifically because we do not need strict ordering agreement across the whole system for those pieces of data. Consensus shows up narrowly: for example, within Cassandra’s own internal cluster coordination, or in a configuration service (like ZooKeeper or etcd) that the platform might use to track which shard owns which key range.

19.4 Failure recovery patterns

Recovery

Retry with exponential backoff

Transient failures (a brief network blip, a momentarily overloaded node) are retried with increasing delay, rather than immediately hammering an already-struggling system.

Recovery

Dead letter queues

Fan-out events that repeatedly fail processing get routed to a separate queue for manual inspection, rather than being silently dropped or endlessly retried.

Recovery

Graceful degradation

Falling back to chronological order if ranking is unavailable, or serving a slightly stale cached feed if the primary post store is temporarily unreachable.

Recovery

Health checks + failover

Load balancers and orchestration systems (like Kubernetes) continuously check service health and reroute traffic away from unhealthy instances automatically, without requiring a human to intervene during common failure scenarios.

🏭
Production example — Amazon & DynamoDB

Amazon’s DynamoDB, which grew directly out of the same “Dynamo” research paper that influenced Cassandra’s design, is explicitly built around an AP-leaning, eventually-consistent model by default (with an option for strongly consistent reads at extra cost), precisely because Amazon’s e-commerce systems learned early on that unavailability during checkout was a much bigger business problem than brief staleness in less critical reads.

💬
What an interviewer may ask

“Is there any part of this news feed system where you would actually want strong consistency?” Yes — account authentication and payment/billing-adjacent data (if the platform sells ads or subscriptions) genuinely benefit from strong consistency, since incorrect or stale data there causes real harm, unlike a slightly delayed post in a feed. This is a good moment to show that “favor availability” is not a blanket rule applied blindly everywhere in the system — it is a deliberate choice made specifically for feed data, based on its actual consistency requirements.

20

Security

20.1 Authentication and authorization

Every request passes through the API Gateway, which validates a signed token (commonly a JWT or an equivalent opaque token backed by a session store) before any request reaches internal services. Internal service-to-service calls typically use mutual TLS or short-lived internal tokens, so that even inside the private network, one compromised service cannot freely impersonate another.

20.2 Privacy controls in the feed itself

A feed system has to respect account privacy settings at every layer that touches content — a private account’s posts should never be fanned out to, cached for, or returned to anyone who is not an approved follower. This means the Fanout Service and Feed Service both need to check privacy state, not just the Post Service at creation time, since privacy settings can change after a post already exists (a user can go from public to private, and previously-visible content should stop being served to non-followers going forward).

20.3 Rate limiting and abuse prevention

The API Gateway enforces rate limits per user and per IP to prevent both accidental abuse (a buggy client hammering the feed endpoint) and deliberate abuse (scraping, spam posting, automated fake engagement). A common implementation is the token bucket algorithm: each user has a bucket that refills at a steady rate, and each request consumes a token — once the bucket is empty, further requests are rejected or delayed until it refills.

TokenBucketRateLimiter.java
// Simplified token bucket rate limiter
public class TokenBucketRateLimiter {

    private final int capacity;
    private final double refillRatePerSecond;
    private double tokens;
    private long lastRefillTimestamp;

    public TokenBucketRateLimiter(int capacity, double refillRatePerSecond) {
        this.capacity = capacity;
        this.refillRatePerSecond = refillRatePerSecond;
        this.tokens = capacity;
        this.lastRefillTimestamp = System.nanoTime();
    }

    public synchronized boolean allowRequest() {
        refill();
        if (tokens >= 1) {
            tokens -= 1;
            return true;
        }
        return false;
    }

    private void refill() {
        long now = System.nanoTime();
        double secondsElapsed = (now - lastRefillTimestamp) / 1_000_000_000.0;
        tokens = Math.min(capacity, tokens + secondsElapsed * refillRatePerSecond);
        lastRefillTimestamp = now;
    }
}

20.4 Content moderation and spam

Newly created posts commonly pass through an asynchronous moderation pipeline — automated classifiers looking for spam, harassment, or policy-violating content, sometimes backed by human review for borderline cases — running in parallel with (not blocking) the normal fan-out pipeline. A post can be live and fanning out to followers while moderation runs in the background, with the ability to retract or hide a post shortly after if it is flagged, rather than delaying every single post while every single one waits for a moderation verdict.

20.5 Protecting the follow graph from scraping

Follower and following lists are sensitive from an abuse perspective — they are exactly the data set that malicious actors want for large-scale scraping or harassment campaigns. Beyond basic rate limiting, production systems often add anomaly detection specifically on graph-read patterns (a single account rapidly querying millions of different users’ follower lists is a strong scraping signal) and may require additional authentication or CAPTCHA-style challenges when that pattern is detected.

💬
What an interviewer may ask

“A user makes their account private after posting something publicly. What has to happen across this system?” Every place that cached or fanned out that user’s public posts needs to eventually stop serving them to non-followers — this typically means invalidating relevant cache entries and adding a privacy check at the Feed Service’s read path (checking current follower status at read time, not just at fan-out time), so that even if a stale cached post ID lingers somewhere, the actual content lookup enforces the current privacy state.

21

Monitoring, Logging, and Observability

A system this large fails in ways that are only visible if you are specifically watching for them. Good observability is not an afterthought bolted on at the end — it is what makes it possible to run this system confidently in production at all.

21.1 The three pillars, applied specifically to this system

Metrics

Time series

Fan-out queue depth, feed read latency (p50/p95/p99), cache hit ratio, and posts-per-second by author-follower-tier (normal vs celebrity). Watching p99 latency specifically matters more than average latency, because averages hide the tail — exactly the users most likely to be affected by a celebrity-triggered hotspot.

Logging

Structured events

Structured, searchable logs for events like “fan-out started for author X, N followers,” making it possible to reconstruct what happened during an incident after the fact.

Tracing

Distributed tracing

Since a single feed read might touch the Feed Service, Ranking Service, Graph Service, and multiple caches, a trace ID that follows a request across all of these (commonly implemented with OpenTelemetry) is essential for understanding where time is actually being spent in a slow request, rather than guessing.

21.2 Alerting on the right signals

The most useful alerts for this system are ones tied directly to user-facing symptoms rather than internal implementation details: feed read latency crossing a threshold, fan-out queue depth growing faster than it is draining (a leading indicator of an unfolding celebrity-post overload), and cache hit ratio dropping sharply (which often signals a cache eviction storm or a newly-viral piece of content overwhelming existing capacity).

21.3 Dashboards built around the celebrity scenario specifically

Given how central the celebrity problem is to this design, a dedicated dashboard tracking active large-scale fan-out jobs, their progress, and their resource consumption is genuinely valuable operationally — it turns “is a celebrity post currently stressing the system” from a question engineers discover the hard way into something they can see clearly and act on proactively.

🏭
Production example — Netflix & Uber

Netflix’s observability stack and Uber’s Jaeger-based distributed tracing platform (Uber created and open-sourced Jaeger specifically to solve this cross-service tracing problem at their own scale) are widely cited industry examples of exactly this approach: correlating metrics, logs, and traces so that an on-call engineer can go from “latency is up” to “here is the specific slow database query” in minutes rather than hours.

💬
What an interviewer may ask

“How would you know, without anyone reporting it, that a celebrity’s post is currently overloading the fan-out pipeline?” A well-designed alert on fan-out queue depth combined with per-author fan-out job duration would surface this automatically — if one job’s expected completion time balloons far past normal, or the queue backlog spikes right after a large-account post event fires, that is a strong, automatic signal, well before any user notices delayed feed updates.

22

Deployment and Cloud Architecture

22.1 Containerization and orchestration

Each service — Post Service, Fanout Service, Feed Service, Ranking Service — is packaged as a container and run under an orchestrator like Kubernetes, which handles scheduling, automatic restarts on failure, and horizontal auto-scaling based on load metrics (like CPU usage or queue depth). This is what lets the Fanout Service, specifically, scale out additional worker instances automatically during a celebrity-post-triggered traffic spike, and scale back down afterward to control cost.

22.2 Multi-region deployment

With 500 million daily active users spread globally, running everything from a single data center would mean users far from that location experience high network latency on every request. Production systems deploy across multiple geographic regions, routing users to their nearest region via DNS-based or Anycast-based routing, with data replicated across regions for both latency and disaster-recovery purposes.

US Region Gateway → Feed Service Regional Redis EU Region Gateway → Feed Service Regional Redis Asia Region Gateway → Feed Service Regional Redis Globally Replicated Post & Graph Storesource of truth syncsyncsync User in California User in Germany User in Singapore Fig 22.1 — Multi-region deployment: local users, local caches, globally replicated source of truth
Fig 22.1 — Multi-region deployment, each region serving local users with regionally cached data backed by globally replicated storage

22.3 CI/CD and safe rollouts

Given how central the Ranking Service and Fanout Service are, changes to either need careful, gradual rollout rather than an all-at-once deployment. Common patterns include canary deployments (rolling a new version out to a small percentage of traffic first, watching key metrics, then expanding) and feature flags (letting a new ranking model or fan-out threshold be toggled independently of a full code deployment, and rolled back instantly if something looks wrong).

💬
What an interviewer may ask

“If a user follows people primarily in a different region than they live in, does the multi-region setup cause any problem?” It can introduce a small amount of extra cross-region latency when fanning out or fetching that specific content, but it does not break correctness — the globally replicated storage layer ensures the data is eventually available everywhere; regional caches simply serve as a fast local layer in front of that shared source of truth, accepting slightly higher latency for the less common cross-region case in exchange for keeping the common case (local content, local users) fast.

23

Disaster Recovery, Backup, and Cost Optimization

23.1 Disaster recovery

The multi-region replication described above doubles as disaster recovery: if an entire region becomes unavailable (a data center outage, a natural disaster, a major cloud provider incident), traffic can be rerouted to healthy regions, since data was already being replicated there continuously rather than only backed up periodically. This is meaningfully better than a traditional “restore from last night’s backup” recovery model, because it minimizes both downtime (RTO — recovery time objective) and data loss (RPO — recovery point objective).

23.2 Backup strategy for the source-of-truth stores

Even with multi-region replication handling most failure scenarios, regular backups of the primary post and graph stores protect against a different class of problem: logical corruption or accidental deletion, which replication would faithfully copy everywhere just as fast as it copies legitimate data. Point-in-time snapshots, retained on a rolling schedule (e.g., daily for 30 days, weekly for a year), give a way to recover from “we accidentally deleted the wrong data” in a way that live replication cannot.

23.3 Cost optimization

At this scale, infrastructure cost becomes a genuine engineering concern, not just a finance question. A few concrete levers specific to this design:

Cost 1

Feed cache TTL & length caps

Every entry kept in an expensive in-memory cache indefinitely is money spent on data that is increasingly unlikely to be read (most feed scrolling happens near the top of the feed); capping length and adding reasonable expiry keeps the expensive tier lean.

Cost 2

Tiered storage for media

Recently-posted media served from a fast, more expensive storage/CDN tier, while older, rarely-accessed media can move to cheaper “cold” storage tiers automatically via lifecycle policies, since access patterns for social content drop off sharply with age.

Cost 3

Right-size celebrity infra

Rather than over-provisioning the entire fleet to handle worst-case celebrity load at all times, dedicated, separately-scaled infrastructure for large accounts means the “normal” fleet can be sized for normal load, with the celebrity-handling capacity scaled and costed independently.

Cost 4

Spot / preemptible compute

Background jobs like ML model retraining or analytics aggregation, which can tolerate occasional interruption, are good candidates for substantially cheaper, interruptible compute instances rather than always-on, full-price infrastructure.

💬
What an interviewer may ask

“Where would you look first if infrastructure cost for this system suddenly spiked?” Cache and storage growth are the most common culprits at this scale — check whether feed cache length caps or TTLs regressed, whether media lifecycle policies are actually moving old content to cold storage as intended, and whether replica counts crept up beyond what current traffic actually requires. Cost regressions in systems like this are very often quiet configuration drift rather than a single dramatic event.

24

Algorithms Deep Dive

A few classic data structure and algorithm ideas show up naturally in this system, and interviewers often ask candidates to actually implement one of them on the spot.

24.1 Merging K sorted lists — the heart of feed assembly

When the pull-model path fetches recent posts from several followed accounts (each individually already sorted by time), combining them into one overall time-sorted list is exactly the classic “merge K sorted lists” problem. The efficient solution uses a min-heap (priority queue): keep one pointer per source list, always pull the globally smallest (or, for our purposes, most recent) next item from the heap, and push that source’s next item back onto the heap.

FeedMerger.java
// Merge K sorted post lists (by timestamp, descending) into one feed
public class FeedMerger {

    public static List<Post> mergeKSortedFeeds(List<List<Post>> sources, int limit) {
        // Max-heap ordered by timestamp so the most recent post is always on top
        PriorityQueue<PostCursor> heap = new PriorityQueue<>(
            Comparator.comparingLong((PostCursor c) -> c.post.getTimestamp()).reversed()
        );

        // Seed the heap with the first (most recent) post from each source
        for (int i = 0; i < sources.size(); i++) {
            List<Post> source = sources.get(i);
            if (!source.isEmpty()) {
                heap.add(new PostCursor(source.get(0), i, 0));
            }
        }

        List<Post> result = new ArrayList<>();
        while (!heap.isEmpty() && result.size() < limit) {
            PostCursor cursor = heap.poll();
            result.add(cursor.post);

            List<Post> source = sources.get(cursor.sourceIndex);
            int nextIndex = cursor.indexInSource + 1;
            if (nextIndex < source.size()) {
                heap.add(new PostCursor(source.get(nextIndex), cursor.sourceIndex, nextIndex));
            }
        }
        return result;
    }

    private record PostCursor(Post post, int sourceIndex, int indexInSource) {}
}

Time complexity: with K sources and N total posts across all of them, this runs in O(N log K) — each of the N items is pushed and popped from a heap of size at most K, and each heap operation costs O(log K). This beats the naive approach of concatenating everything and sorting from scratch, which would cost O(N log N) — meaningfully worse when K (the number of followed accounts contributing) is much smaller than N (the total candidate posts).

24.2 LRU cache — the eviction policy behind most caching layers

When a cache reaches its capacity limit, it needs a policy for deciding what to remove to make room for new entries. Least Recently Used (LRU) is the most common choice for feed and content caches, because social content access naturally clusters around recency — old, unused entries really are the ones least likely to be needed again soon.

LRUCache.java
// LRU cache using a HashMap + doubly linked list, O(1) get and put
public class LRUCache<K, V> {

    private final int capacity;
    private final Map<K, Node<K, V>> map = new HashMap<>();
    private final Node<K, V> head = new Node<>(null, null); // most recently used end
    private final Node<K, V> tail = new Node<>(null, null); // least recently used end

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }

    public V get(K key) {
        Node<K, V> node = map.get(key);
        if (node == null) return null;
        moveToFront(node);
        return node.value;
    }

    public void put(K key, V value) {
        if (map.containsKey(key)) {
            Node<K, V> node = map.get(key);
            node.value = value;
            moveToFront(node);
            return;
        }
        if (map.size() >= capacity) {
            Node<K, V> lru = tail.prev;
            remove(lru);
            map.remove(lru.key);
        }
        Node<K, V> node = new Node<>(key, value);
        map.put(key, node);
        addToFront(node);
    }

    private void moveToFront(Node<K, V> node) { remove(node); addToFront(node); }

    private void addToFront(Node<K, V> node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    private void remove(Node<K, V> node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private static class Node<K, V> {
        K key; V value;
        Node<K, V> prev, next;
        Node(K key, V value) { this.key = key; this.value = value; }
    }
}

24.3 Concurrency considerations in the fan-out worker pool

Fan-out workers process events concurrently across many threads or processes for throughput, which introduces classic concurrency concerns: two workers must never double-process the same event (handled via the idempotency approach discussed earlier), and shared resources like a connection pool to Redis must be sized and managed carefully — too few connections and workers block waiting for one; too many and you overwhelm the downstream Redis cluster itself. A bounded thread pool combined with a bounded connection pool, sized based on load testing rather than guesswork, is the standard, practical answer.

💬
What an interviewer may ask

“Why is a heap better than just sorting the combined list every time for the merge step?” Because sorting from scratch does redundant work — it re-examines the ordering of items whose relative order, within their own already-sorted source list, never changed. The heap approach only ever compares the current “frontier” item from each source, which is a much smaller amount of work when the number of sources (K) is small relative to the total items (N), which is the common case here since most users follow a bounded, moderate number of accounts.

25

Best Practices and Common Mistakes

25.1 Best practices worth internalizing

BP 1

Design for the tail

The entire hybrid model exists because a design that only accounts for average follower counts fails badly at the extreme. Always ask “what does the 99th or 99.9th percentile case look like?” before calling a design finished.

BP 2

Thin write path

Anything that is not strictly necessary to acknowledge a post as successfully created should happen asynchronously afterward — this is the single biggest lever for keeping user-facing latency low and predictable.

BP 3

Measure thresholds

The celebrity threshold, cache TTLs, and replica counts should all be grounded in actual load testing and production metrics, not guessed numbers that sound reasonable.

BP 4

Graceful degradation

Prefer graceful degradation over hard failure everywhere possible. A slightly worse feed is almost always a better user outcome than an error page, and building this expectation into every service from the start is far easier than retrofitting it after an incident.

BP 5

Disciplined denormalization

Whenever data is duplicated for performance (like the two-directional follow lists), be explicit and disciplined about exactly how and when both copies get updated together.

25.2 Common mistakes candidates make in interviews (and engineers make in real systems)

Mistake — Skipping requirements

Jumping straight to microservices boxes without stating requirements or estimating scale first makes it impossible for anyone to judge whether the design decisions that follow actually fit the problem.

Mistake — One universal fan-out strategy

Proposing a single fan-out strategy without recognizing that the follower-count distribution itself is the core difficulty in this specific problem.

Mistake — Static follower counts

Forgetting that follower counts change over time and not accounting for accounts crossing the celebrity threshold in either direction.

Mistake — Ranking as an afterthought

Bolting ranking on at the very end of a design discussion, when it is actually central to what makes the product usable at scale, not just a nice-to-have feature.

Mistake — Assuming perfect delivery

Assuming perfect delivery guarantees from a message queue without accounting for at-least-once delivery and the resulting need for idempotent processing.

Mistake — Ignoring privacy in caches

Ignoring privacy state as data flows through caches and forgetting that privacy settings can change after content already exists in the system.

💬
What an interviewer may ask

“If you had to cut this design down to the three most important decisions, which would you pick?” A strong answer usually centers on: (1) the hybrid fan-out strategy, since it is the direct answer to the celebrity constraint in the prompt, (2) favoring availability and eventual consistency system-wide, since it justifies nearly every caching and replication choice made, and (3) decoupling write and read paths through an event queue, since it is what makes the whole asynchronous, resilient shape of the system possible in the first place.

26

Real-World and Industry Examples

Platform

Twitter / X

Twitter is the platform most publicly associated with the celebrity fan-out problem, largely because its short-form, high-frequency posting model made the issue visible early and often. Twitter engineers have described, in public talks and blog posts over the years, evolving from a pure push-based timeline service toward a hybrid model, using a follower-count threshold to decide fan-out strategy — essentially the exact pattern detailed in this tutorial — alongside a dedicated graph database (FlockDB) built specifically for fast follower-relationship queries at their scale.

Platform

Instagram

Instagram’s feed evolved from a straightforward chronological model to a machine-learning-ranked feed, a change the company discussed publicly specifically because engagement metrics improved once ranking accounted for a viewer’s relationship with each poster, not just recency. Instagram’s massive scale of photo and video content also makes their media storage and CDN strategy a strong real-world example of the tiered storage approach discussed in the cost optimization section.

Platform

Facebook

Facebook’s News Feed, the feature that arguably started this entire product category in 2006, has gone through multiple generations of ranking system, and Facebook’s internal data infrastructure — including TAO, a purpose-built distributed data store for the social graph — is a well-documented real-world example of exactly the kind of specialized storage layer this tutorial’s data model section argues for, rather than relying on general-purpose databases for every access pattern.

Platform

LinkedIn

LinkedIn’s feed operates over a professional network graph with different dynamics than a consumer social app (generally lower posting frequency, longer content lifespan), and LinkedIn is also notably the company that created Kafka internally to solve exactly the kind of high-throughput, decoupled event pipeline problem this tutorial’s architecture relies on for connecting the write and fan-out paths.

Pattern

Uber

While Uber does not run a news feed, its engineering team’s creation of Jaeger for distributed tracing is a widely cited real-world example of the observability tooling this tutorial’s monitoring section recommends for tracing a single request across many microservices — the exact kind of tooling gap a system like our news feed design would run into at real scale.

💬
What an interviewer may ask

“Which of these companies’ approaches would you borrow most directly for this specific problem, and why?” Twitter’s hybrid, threshold-based fan-out is the most directly applicable, since it addresses the exact constraint named in the prompt — extreme follower counts. It is reasonable to combine that with Facebook or Instagram-style ML-based ranking for the read path, and Kafka (LinkedIn’s contribution to the ecosystem) for the event backbone connecting the two — showing that a strong design often borrows the right proven idea from each company’s public engineering work rather than copying one company’s stack wholesale.

27

Frequently Asked Questions

Q1

What follower-count threshold separates “normal” from “celebrity” accounts?

There is no fixed universal number — real systems determine it through load testing, typically somewhere in the range of tens of thousands to a few million followers, and adjust it over time as infrastructure capacity and traffic patterns change. The important part is having a threshold-based decision at all, not the exact value chosen.

Q2

Does the hybrid model fully eliminate the celebrity problem, or just reduce it?

It eliminates the specific failure mode of doing tens of millions of fan-out writes for one post. It does not eliminate all load from a celebrity post entirely — reads from that account still need to be fetched live by followers who follow them, and a viral celebrity post can still create hot-key pressure on the post content cache, which is why the hot-key mitigation covered in the caching section matters alongside the fan-out strategy itself.

Q3

Why not just always fan out to the first N followers and pull for the rest?

This is actually a reasonable refinement some real systems use — capping fan-out at a fixed number of “most engaged” or “most recently active” followers rather than an account’s entire follower list, and relying on pull for everyone else. It is a variation on the same core hybrid idea: bound the worst-case write cost, and shift the remainder to read time.

Q4

How does the system avoid showing duplicate posts if two people share the same content?

The merge step in the Feed Assembler deduplicates by post ID before ranking, as shown in the earlier code sample. If two different accounts genuinely created two separate posts referencing the same content (like a repost or share of someone else’s original post), the system typically links the repost to the original post ID so the feed can group or attribute them together rather than showing fully unrelated duplicate entries.

Q5

What happens to a user’s feed cache if they follow many new accounts at once?

Their existing feed cache is not rebuilt retroactively — fan-out only pushes new posts created after a follow relationship exists. To avoid an empty-feeling feed immediately after following many new accounts, the read path typically supplements the cache with a live pull of each newly-followed account’s recent posts for a short backfill window, blending both sources until the cache naturally fills up from ongoing fan-out.

Q6

Is this design overkill for a smaller social platform?

Genuinely, yes, for a platform with a few hundred thousand users, a much simpler design — a single well-indexed database and a straightforward pull-based feed query — would work fine and be far easier to operate. This full architecture earns its complexity specifically at the scale and follower-distribution extremes described in the prompt: 500 million daily active users with individual accounts exceeding 100 million followers.

28

Summary and Key Takeaways

Let us bring everything together into the core narrative you would want to walk an interviewer through, start to finish, if asked to design this system live.

The seven ideas that hold this whole system together

  • Start with requirements and numbers. A 500 million DAU platform with a heavily read-skewed traffic pattern (roughly 20:1 or higher reads to writes) tells you immediately to optimize aggressively for read performance, even at some cost to write performance.
  • Fan-out-on-write is the right default because it turns expensive, repeated reads into a one-time write cost — but it breaks down catastrophically for accounts with extreme follower counts.
  • Fan-out-on-read solves the celebrity case by making writes nearly free regardless of follower count, at the cost of more expensive reads — the opposite trade-off, useful exactly where fan-out-on-write is weakest.
  • The hybrid model — the actual answer to this prompt — combines both, using a follower-count threshold to route each account to whichever strategy is cheapest for it, and merging the results together at read time so the end user never notices the difference.
  • Everything else in the design supports that core decision: partitioning and consistent hashing to distribute load evenly, multi-layered caching (including specific hot-key mitigations for viral content), an event-driven architecture to decouple write speed from fan-out speed, and a system-wide bias toward availability and eventual consistency over strict correctness.
  • Ranking turns a raw candidate list into an actually engaging feed, and deserves to be treated as a first-class, independently scalable service rather than an afterthought.
  • Production concerns — security, observability, multi-region deployment, disaster recovery, and cost — are not separate from the core design; they are what makes the difference between a design that looks good on a whiteboard and one that would actually survive running at this scale.
The one sentence to remember

A news feed at this scale is not one hard problem — it is the same simple idea, “show me what my network posted,” solved twice: once cheaply for the common case, and once differently, deliberately, for the rare but enormous exception, merged seamlessly so neither approach’s weaknesses ever reach the end user.