Design a news feed
The method applied end to end to an infinite, personalised feed: requirements and architecture, the data model and why feeds need cursor pagination, and the deep dives — virtualization, media, optimistic likes and freshness.
"Design a news feed" is the most-asked frontend system-design prompt. This page runs it through the design method end to end: scope it, architect it, model the data, and go deep on the parts a feed actually turns on. The hard parts are already built as React practices, so the deep dives link to working code; the demos here cover the two feed-specific decisions those don't — why a feed needs cursor pagination, and how an optimistic like rolls back.
Requirements and architecture
Start where the method says to: scope the problem, then sketch the components and pick where rendering happens.
- Functional: scroll an endless feed; create a post with text and media; like and comment, with the count updating; pull to refresh / load newer.
- Non-functional: performance, accessibility, network, freshness — a fast first screen, smooth scroll over thousands of items, works on flaky mobile, and a clear freshness target.
- Scope cuts make it tractable: fix the audience (say, mobile-first), defer admin/moderation, and name how personalised and how fresh before designing.
- Rendering: server-render the first screen for load and SEO, hydrate, then client-fetch further pages. An alternative is pure client rendering (simpler hosting, slower first paint and weaker SEO) — the right call depends on the freshness and SEO requirements above.
Try it: the feed's architecture
Do: Scope audience and freshness first; put a data layer between views and the API; justify the rendering choice from the requirements.
Avoid: Jumping to components before scope, or picking a rendering mode by habit rather than from the freshness/SEO needs.
Read more:web.dev: Rendering on the web(opens in a new tab)The design method(opens in a new tab)
Data model and pagination
Model the feed as an ordered list of post ids plus a cursor, with the posts themselves in a normalised store keyed by id. Then a like updates one post object and every place showing it stays consistent — and the choice of pagination stops being cosmetic.
- Normalise. One post, one entry keyed by id; the feed is just an ordered id list. No duplicated copies to keep in sync.
- Feeds shift under you. New posts arrive at the top between page loads. Offset pagination then re-counts from a moved start and repeats or skips items; cursor pagination points at the last post seen and is immune.
- Pick cursor for an endless feed — it's stable and efficient; its cost is you can't jump to an arbitrary page, which a feed doesn't need anyway.
- Try it: load a page, let new posts arrive, then load the next under each scheme.
Try it: cursor vs offset while the feed grows
Page size 4. Newest posts have the highest numbers.
Load the first page, then make new posts arrive before loading the next.
Offset counts from the start, so when posts arrive at the top the window shifts: load a page, let posts arrive, then load the next to see a post repeat. Trade-off: offset lets you jump to any page, but it drifts under inserts and gets slow at deep offsets.
Do: Use cursor pagination for feeds; normalise posts by id; prefetch the next page near the end of the current one.
Avoid: Offset pagination on a live feed, or storing the same post in several shapes.
Read more:Advanced Data Grid (pagination in practice)(opens in a new tab)MDN: structured data & keys(opens in a new tab)
Deep dives: list, media, interactions, freshness
A feed is won in a few deep dives: rendering a huge list cheaply, loading media without the page jumping, and making interactions feel instant. Each is a built practice in this handbook — the reasoning here, the code a click away.
- Render only what's visible. A feed of thousands of rows must be virtualized, with infinite scroll loading more near the end. See Virtualized Large Lists and Infinite Scroll.
- Load media without layout shift. Reserve each image's space and load lazily so arriving media doesn't shove content around. See Lazy-Loaded Images.
- Cache and dedupe fetches. A shared data layer keeps the feed and post details consistent and avoids refetching. See Custom Data-Fetching Hook with Caching.
- Interactions feel instant with optimistic UI — update immediately, reconcile with the server, and roll back on failure. The pattern also drives Real-Time Chat.
- Freshness is a requirement, not an accident. Seconds-fresh (a "new posts" pill or a realtime push) costs more than minutes-fresh (refetch on focus) — choose it deliberately. And keep ranking on the server: the client renders the order it's given and layers in only local signals (a just-sent post, an optimistic like); re-ranking on the client fights the server and breaks pagination. At 10× scale the same design grows with windowed rendering, a media CDN and prefetching, not a rewrite.
Try it: an optimistic like with rollback
Optimistic UI assumes success for instant feedback, then reconciles: on failure it restores the exact previous value. The count stays correct across repeated clicks because each attempt snapshots and restores its own prior state.
Do: Virtualize the list, reserve media space, share a caching data layer, make interactions optimistic with a clean rollback, and set an explicit freshness target with ranking on the server.
Avoid: Rendering every row, letting images shift the layout, blocking the UI on each like, or re-ranking on the client.
Read more:web.dev: Cumulative Layout Shift(opens in a new tab)Virtualized Large Lists(opens in a new tab)The design method(opens in a new tab)
Next case studies: autocomplete, a chat application, and a design system — each applying the same method to a different shape of problem.