System design template

Social media feed architecture.

Fan-out on write is the right answer until someone with ten million followers posts, and the entire design exists to handle that exception.

Download for macOS
v0.1.33 · .dmg · Apple Silicon & Intel
Signed & notarized by Apple · opens without a Gatekeeper warning
sha256 698955a0187bc039f4c74f5d05a9f10fbb27376a45788a0a241d1326b73873c7
Download for Windows instead
$curl -fsSL https://lucidtrain.com/install.sh | sh
Social media feed architecture. 11 components across 4 tiers.
Social media feed architecture. Rendered by the same ELK layout engine the app runs: the agent emits components, tiers and edges, and the engine places them, so the boxes cannot overlap.

The components

Every row below is read from the graph that produced the diagram above, so the two cannot disagree.

ComponentTierWhy it is there
ClientClientSupporting component
API gatewayEdgeSupporting component
Post serviceApplicationSupporting component
Social graphApplicationFollower edges
Fan-out workerApplicationPush on write, except for celebrities
Feed serviceApplicationMerges pushed timeline with pulled hot accounts
RankingApplicationSupporting component
KafkaDataSupporting component
Redis timelinesDataMaterialised per user
CassandraDataSupporting component
Object storeDataSupporting component

Design decisions worth arguing about

A diagram shows what was chosen. It does not show what it cost, and that is usually the part that matters in a review or an interview.

Fan-out on write makes reads cheap and writes expensive

Pushing each post into every follower's timeline at publish time means reading a feed is a single range query, which is what you want for the operation that happens constantly. The cost is proportional to follower count, so an average user is trivial and a popular one is a million writes for a single action. That asymmetry, not the average case, is what dictates the architecture.

A pull path for the accounts that break it

Above some follower threshold, fan-out stops being viable, so those accounts are not pushed at all. The feed service instead fetches their recent posts at read time and merges. This makes reads more expensive for followers of popular accounts, which is most people, and it is still cheaper than the alternative. The complexity cost is real: two code paths producing one feed, and a threshold that needs tuning.

Timelines are a cache, not a record

Materialised timelines are derived data and should be rebuildable from posts and the graph. Treating them as authoritative means a bug corrupts something you cannot regenerate. Keeping them disposable lets you cap their length, expire inactive users entirely and rebuild on demand, which bounds memory in a way that storing every user's full timeline never could.

Ranking after retrieval, not instead of it

Ranking a candidate set you already have is fast; ranking by querying a model for every possible post is not. Retrieving chronologically and then reordering keeps the expensive part bounded. The consequence is that anything not retrieved cannot be ranked, so retrieval quality caps ranking quality, which is the same lesson as retrieval-augmented generation in different clothes.

How it changes with scale

Fan-out volume grows with the product of users and average follower count, which grows faster than either. Timeline storage grows with active users and is controlled by capping length and expiring inactive accounts. The read path is comparatively flat, which is the entire return on doing the work at write time.

Where it breaks first

A celebrity post during a spike. Even with a pull threshold, the accounts just under it produce enormous fan-out bursts that saturate the workers and delay every other user's timeline. The symptom is not an error but posts appearing minutes late for people unrelated to the event, which is why fan-out lag is a metric worth alerting on.

Draw this yourself

Open the Diagram tab and describe the system. The agent emits a semantic graph rather than coordinates, so you can edit the components and the layout re-solves instead of drifting.

shell
$ Diagram a social media feed: post service, social graph, fan-out workers pushing to materialised Redis timelines, a feed service that merges pushed timelines with a pull path for high-follower accounts, and a ranking service.

When the shape is right, Implement in code turns the canvas into a markdown specification, every component, every relationship and the notes, and starts a real turn in the Code tab with it.

FAQ

Questions about this design

Why not fan-out on read for everyone?

Because it makes the most frequent operation the most expensive one: assembling a feed means querying every followed account on every refresh. It is simpler and it does not survive real usage patterns.

Where should the follower threshold be?

Wherever fan-out latency starts affecting other users, which is a property of your worker capacity rather than a universal number. It is a tuning knob that should be adjustable without a deploy.

How are deletes handled?

By filtering at read time rather than removing from every materialised timeline, which would be another fan-out. The timeline holds ids, and a deleted post is skipped when hydrated.

Does a chronological feed simplify this?

It removes the ranking service and nothing else. The fan-out problem, the celebrity problem and the storage problem are all identical, which is why chronological feeds are not meaningfully cheaper to build.

More templates