Design complete systems / CHAPTER 59
Design a ranked news feed (Facebook)
Build follower feeds from saved posts, limit the work needed for each page and recheck who may see each result.
Builds on Design a microblogging service (Twitter), Push versus pull architecture, Normalization versus denormalization.
- Explain how a feed finds possible posts, orders them and loads their display content.
- Choose when to prepare follower lists and when to read an author’s posts on demand.
- Preserve current audience checks and one page order even when background follower-list updates are delayed.
01Functional requirements
A news feed combines posts from people and pages the viewer follows. Some posts will be ready in a prepared list; others can be collected when the viewer opens the feed. Neither route may bypass the post’s current audience rules.
FR1: Post publication. Publish text and media references with a stable request identity. A successful response means the source post and a recoverable record of required feed-update work have both committed.
FR2: Follow relationships. Let an authenticated viewer follow or unfollow an author. In this design, an unfollow excludes that author’s historical posts from subsequent feed requests.
FR3: Ranked feed pages. Return a size-limited page of currently permitted posts from followed people and pages, ordered by the chosen ranking. Return a continuation cursor that identifies the same result traversal when requesting the next page.
FR4: Post deletion. Let the author delete a post. Stale candidate references must not cause a later response to return its deleted body.
FR5: Audience updates. Let the author change a post’s audience. Relevance scores and old feed membership do not grant access under the new audience rule.
FR6: Comments. Store comments separately so permitted readers can display them with a post. Ranking may use a comment-count feature without loading every comment body while building the feed.
FR7: Reactions. Store each user’s reaction separately from the post body so it can be displayed or used as a ranking feature without copying mutable post content into every feed.
Out of scope
- Advertising auctions and a complete machine-learning training platform.
- Erasing content already downloaded or displayed on someone else’s device. Server-side deletion and unfollow rules govern subsequent responses.
Example. Mina follows ordinary author Arun and a page with ten million followers. Arun publishes P8. We will trace P8 from its source transaction to Mina’s feed, then compare ordinary-author fanout with the large page’s read-time merge.
02Nonfunctional requirements
These are independently chosen exercise assumptions to validate, not measured production guarantees.
NFR1: Page latency. At least 95% of measured feed requests must finish within 300 ms, from receipt to completed response. Include finding possible posts, checking permission, ranking and loading display records. Fetching referenced media on the device is a separate measurement.
NFR2: Candidate freshness. A new permitted post must become available for the ranking stage within five seconds of its source commit. Such a possible result is called a candidate. This does not guarantee its position in the feed or delivery to every connected device.
NFR3: Peak page capacity. Plan for about 11,570 pages/second at the assumed peak derived below. Test the viewer-follow distribution and large-author bursts rather than relying on an average author.
NFR4: Bounded ranking work. Score at most 500 possible post IDs per page. Limit extra fetching when permission checks remove items. If richer scoring misses its deadline, return permitted candidates newest first, breaking equal creation times by post ID.
NFR5: Eligibility correctness. Return zero deleted or currently forbidden post bodies. The handler checks the main post record and current audience before returning a body, even if an old feed entry, cache or model score names it. Omit the item when permission cannot be established.
NFR6: Publication recovery. Once publication succeeds, a worker or message-relay crash must not lose the saved post or unfinished follower-list work. The publication handler saves both together before reporting success, using the database’s agreed durable commit settings. Later list updates and notifications may be delayed without erasing the post.
03Back-of-the-envelope Estimates
Count both the page requests and the work performed to prepare each page. These decimal-byte estimates exclude media delivery unless stated.
- Feed reads. Five million daily users open twenty pages each:
5 million × 20 = 100 million pages/day. That is about 1,157 pages/second on average and 11,570/second at a chosen tenfold peak. The handler must assemble pages at that rate, not merely accept connections. - Page payload. Twenty displayed posts at 1 KB of references and metadata each make 20 KB/page.
100 million × 20 KB = 2 TB/daybefore media and transport overhead. Deliver media from object storage through a CDN; do not copy image bytes into each follower’s feed record. - Prepared follower lists. One million posts/day, each sent to 200 followers, create
1 million × 200 = 200 millionreference inserts/day. At 32 bytes each, that is 6.4 GB/day raw, or 44.8 GB for seven days before indexes and replicas. The ordinary average is about 2,315 inserts/second. - A large author. One post from the page with ten million followers would request ten million inserts at once. The average above hides this burst. Compare that work with reading the author’s timeline only for people who actually open a page.
- Ranking work. At most 500 candidates/page times 11,570 peak pages/second gives about 5.8 million evaluations/second. At an assumed 10 microseconds each, scoring alone consumes about 58 CPU-seconds every second, roughly 58 fully busy cores. Retrieving features and candidates adds more work; benchmark the actual scorer and retain a cheap fallback.
04API Design and Retry Contracts
Arun’s app asks the post handler to publish text. The handler saves a post identified as P8; version 8 is that record’s current change number, used later to distinguish a newer deletion from this creation.
POST /posts
Idempotency-Key: publish-P8
Content-Type: application/json
{"text":"The garden is open","mediaRefs":[],"audience":"followers"}
{"postId":"P8","version":8,"state":"published"}
- Publish once.
POST /poststakes text, media references, audience and a stable idempotency key. The server derives Arun’s identity from login. Return201only after saving the post and its required follow-up work. A matching retry returns the same publication result; saving the post does not mean all follower lists are already updated. - Manage follows.
POST /followstakes an author ID; the follower comes from authentication.DELETE /follows/{authorId}removes that relationship. The next feed request excludes the unfollowed author’s historical posts under this chapter’s rule. - Read a page.
GET /feed?limit=20&cursor=…returns permitted posts and an opaque continuation cursor. The handler saves a limited ordered list of candidate IDs and scores under a view ID with an expiry. The cursor carries that view ID and the last(score,postId)position. Later pages continue that saved order and still remove posts the viewer may no longer read; a page number alone cannot stabilize changing scores. Return400for malformed cursors and an explicit restart response for expired views. - Edit or delete.
PATCH /posts/P8changes the audience only for the author and expected current version.DELETE /posts/P8records deletion with a newer version. Save each source change and its follow-up event together so workers can repair their copies. - Add engagement.
POST /posts/P8/commentsuses a stable request key.PUT /posts/P8/reactionderives the reacting user from login. Both check current post access and save separate comment or reaction records; neither overwrites a feed copy of the post body.
05Data Model and Access Paths
The main post database decides which post exists, its current text and who may read it. A feed list only stores IDs of possible posts. Loading the current display records for those IDs is called hydration.
| Record / key | Fields and index | Reason |
|---|---|---|
Post postId |
Author, text/media refs, audience, creation time, version, deletion state; (authorId,createdAt,postId) index |
Source of truth and author timeline |
Follow (followerId,authorId) |
State/version; reverse index on author | Read sources and enumerate followers |
Feed entry (viewerId,postId) |
Score hint, creation position, source version | Deduplicated derived candidate |
Comment (postId,commentId) / reaction (postId,userId) |
Body/state or reaction type | Display and bounded engagement features |
Outbox eventId |
Post ID, event type, version, relay state | Retry publication after commit |
- The publication handler also saves the result for
(authorId,requestKey). That lookup makes a repeated create request returnP8rather than create another post. - The outbox is a table of work still needing delivery, such as “
P8, version 8, created.” The handler writes that row in the same transaction asP8. If it crashes before notifying a worker, another program can find the row later. - The relay is the program that reads pending outbox rows and sends them to feed workers. It may send twice if the first send succeeded but the relay crashed before recording progress.
- The feed worker inserts one
(viewerId,postId)pair per follower. A database uniqueness rule rejects a second identical pair, so repeating delivery does not putP8twice in Mina’s list. - The source record retains newer versions and deletion state. Workers compare these before applying old work, and the read handler checks current post, audience and follow rules before returning a body.
06Baseline: Start with pull, then measure
Begin without prepared follower lists. Mina follows Arun and a page with ten million followers. When she opens the feed, collect recent posts from those authors. Doing this when a reader asks is called fanout on read, or pull.
- Arun’s publication handler saves
P8and its publication result in the main database. The record contains its author, text, media references, audience and version. - Mina’s browser calls
GET /feed. The feed handler verifies Mina and reads the author IDs she currently follows. - For each author, it uses the
(authorId,createdAt,postId)index to load a limited recent slice of post IDs. The limit prevents one author’s whole history from being scanned. - It combines those IDs, removes duplicates and reads current post state and audience rules.
P8remains only if Mina may currently read it and still follows the author. - It scores a bounded set, initially using simple recency with a small engagement contribution. It selects the page and loads its display fields; this loading step is hydration.
- It returns the page with a cursor tied to the ranking view. Mina’s device requests any referenced media through its own permitted delivery path.
Trace P8 through those steps: it becomes a possible result when the handler reads Arun’s timeline, survives Mina’s audience check, receives a score and supplies its current text. No precomputed list is necessary yet.
The drawback appears when Mina follows 500 active sources: producing twenty results may require hundreds of timeline reads. Group reads where supported and cap the work. Parallel calls can shorten waiting, but still consume network and storage. Pull remains attractive for inactive followers because no feed work happens until they return.
BUILD THE DESIGN, ONE CHANGE AT A TIME
Watch the architecture evolve.
Advance through the stages in this same diagram. Existing components keep their places; additions fill the available space, then add rows. Highlights show what changed from the previous stage, and numbered arrows match the connection key below.
Stage 1 of 3
Start here. A viewer follows few enough active authors that the handler can read their recent timelines within the page budget.
Pull feed
Follow the numbered arrows
- Feed reader → Feed serviceGET /feed with traversal context
- Feed service → Follow authoritycurrent followed sources
- Feed service → Post + timeline storepull bounded recent post IDs
- Feed service → Eligibility + hydrationcandidate IDs to validate
- Eligibility + hydration → Post + timeline storecurrent body, version, audience
- Eligibility + hydration → Feed readerpermitted display records + media refs
- Feed reader → Media deliveryload permitted media representation
Trace the diagram in words
- The handler reads the authors Mina follows.
- It reads Arun’s recent IDs and checks whether Mina may read P8.
- It scores eligible IDs and loads the selected text and media references.
- The main database saves posts and follow relationships.
- The feed handler reads a bounded timeline slice per followed author, checks permission and selects display records.
- Media bytes follow their own permitted delivery path rather than being copied into feed lists.
The cost: Following hundreds of active authors multiplies timeline reads. Parallel requests may shorten waiting but still use storage and network capacity.
Why change it? Repeated feed reads now spend too much work loading ordinary authors’ timelines over and over.
Pushed candidate lists
Follow the numbered arrows
- Feed reader → Feed serviceGET /feed with traversal context
- Feed service → Follow authoritycurrent followed sources
- Feed service → Post + timeline storepull bounded recent post IDs
- Feed service → Eligibility + hydrationcandidate IDs to validate
- Eligibility + hydration → Post + timeline storecurrent body, version, audience
- Eligibility + hydration → Feed readerpermitted display records + media refs
- Feed reader → Media deliveryload permitted media representation
- Post + timeline store → Fanout workersrelay committed publication intent
- Follow authority → Fanout workersenumerate current follower work
- Fanout workers → Viewer candidate listsidempotent viewer-post insert
- Feed service → Viewer candidate listsbounded ready candidates
Trace the diagram in words
- The publication handler saves P8 and required follow-up work in one transaction.
- The worker inserts(Mina,P8); retrying that same pair creates no duplicate.
- Mina’s read still checks current post access before loading and returning the body.
- The post handler commits P8 and its pending feed-update row together.
- A relay sends that row to workers, which insert unique viewer/post references into prepared lists.
- The source timelines and follows remain available to rebuild damaged lists or support read-time discovery.
Inspect the connection changes
- Now: Post + timeline store → Fanout workers: relay committed publication intent
- Now: Follow authority → Fanout workers: enumerate current follower work
- Now: Fanout workers → Viewer candidate lists: idempotent viewer-post insert
- Now: Feed service → Viewer candidate lists: bounded ready candidates
All other connections are retained. Response paths are implicit unless drawn.
The cost: Preparing follower lists increases writes and does work for inactive viewers. Workers need saved scan progress, duplicate-safe inserts and deletion-version checks.
Why change it? The large page has ten million followers, so preparing every list per post costs too much. Richer scoring also needs an explicit work limit.
Hybrid bounded ranking
Follow the numbered arrows
- Feed reader → Feed serviceGET /feed with traversal context
- Feed service → Follow authoritycurrent followed sources
- Feed service → Post + timeline storepull exceptional-source timelines or repair
- Feed service → Eligibility + hydrationcandidate IDs to validate
- Eligibility + hydration → Post + timeline storecurrent body, version, audience
- Eligibility + hydration → Feed readerpermitted display records + media refs
- Feed reader → Media deliveryload permitted media representation
- Post + timeline store → Fanout workersrelay committed publication intent
- Follow authority → Fanout workersenumerate current follower work
- Fanout workers → Viewer candidate listsidempotent viewer-post insert
- Feed service → Viewer candidate listsbounded ready candidates
- Eligibility + hydration → Bounded rankereligible bounded candidates + features
- Bounded ranker → Eligibility + hydrationchosen IDs; hydrate/recheck source
Trace the diagram in words
- The handler merges both ID sources and removes duplicates.
- It checks current access and scores only the bounded candidate set.
- It loads permitted display records. If scoring is late, it orders the permitted candidates newest first with a post-ID tie-breaker.
- The handler combines ordinary prepared lists with recent timelines from exceptionally large authors.
- It checks access and scores no more than the chosen candidate budget.
- It loads selected display records and uses an eligible chronological fallback if scoring misses its deadline.
Inspect the connection changes
- Now: Feed service → Post + timeline store: pull exceptional-source timelines or repair
- Now: Eligibility + hydration → Bounded ranker: eligible bounded candidates + features
- Now: Bounded ranker → Eligibility + hydration: chosen IDs; hydrate/recheck source
- Previously: Feed service → Post + timeline store: pull bounded recent post IDs
All other connections are retained. Response paths are implicit unless drawn.
The cost: The hybrid needs two retrieval paths, merging and retained page-ranking context. Relevance may fall during a scorer outage, but permission rules remain the same.
07Evolution: Fan out ordinary authors; pull exceptional ones
First test whether more memory for frequently read timelines or faster scoring processors extends the simple design. Add prepared lists only when repeated timeline reads become the measured problem.
Prepare ordinary authors’ posts when they publish
- Arun’s handler commits
P8and its outbox work row together. - The relay reads that row and sends its event ID, post ID and version to a background feed worker. An event is a record of the already committed change, not another instruction to create
P8. - The worker reads Arun’s followers and inserts
(Mina,P8),(Leo,P8)and so on into size-limited candidate lists. This copying of references to many recipients is fanout on write, or push. - On repeated delivery, an insert for
(Mina,P8)finds the same unique pair and creates no second entry. The worker records progress so it can resume the follower scan after a crash. - Mina’s next feed request reads her prepared list, then performs the same current permission checks, scoring and hydration. The list makes discovery cheaper; it does not authorize content.
Keep very large authors on a read-time path
- Retain the large page’s recent timeline instead of requesting ten million follower inserts for every post.
- When Mina reads, combine her prepared ordinary-author IDs with a bounded recent slice from that page’s timeline. Remove repeated IDs before scoring.
- Check current source versions, deletion, audience and follow relationships. Fetch at most the defined candidate budget; if filtering leaves a short page, do not search forever for replacements.
- Score at most 500 candidates, load the selected bodies and recheck any permission that could have changed before return. Use the agreed eligible chronological order if richer scoring misses its deadline.
| Policy | Who does the extra work? | Cost to accept |
|---|---|---|
| Pull | Feed handler reads author timelines per page | Repeated reads for active viewers |
| Push | Worker prepares references per follower | Many inserts, including inactive followers |
| Hybrid | Worker pushes ordinary posts; handler pulls exceptional authors | Two candidate sources and more merge rules |
Choose the threshold using reader activity, posting rate, worker capacity and the five-second freshness target, not follower count alone. Partition follower lists by viewer and timelines by author. Cache immutable media or post versions, but keep current permission checks outside an old response cache. Notifications only tell Mina new candidates exist; a reconnect fetches saved state, and a repair program can rebuild lists from source posts and follows.
One ordinary post becomes references in follower lists
Follow the numbered arrows
- Arun publishes P8 → Fanout workerretry durable event
- Fanout worker → Mina candidate listID reference only
- Fanout worker → Leo candidate listsame post; other viewer
- Mina candidate list → Mina readsretrieve candidates
Trace the diagram in words
- The handler commits the post and pending publication work.
- The relay sends that saved event; the worker inserts a unique reference for each follower.
- Mina’s read loads the references, checks current access and then fetches display content.
08Failure and Recovery Scenarios
Test the same P8 path with failures that change what a handler or worker may do:
- The create event is delivered twice. The worker attempts the same unique
(viewerId,P8)insert. The database keeps one reference per viewer. A unique event ID alone would not help if the worker created a fresh feed-entry row on every delivery. - Deletion arrives before an old create. The main record says
P8was deleted at version 9. A delayed worker holding creation version 8 compares versions and refuses to restore it. Retaining the deletion record, called a tombstone, preserves that evidence. - Mina loses access. Arun changes
P8’s audience or Mina unfollows him while an old candidate entry remains. The read handler checks current post and relationship records and omitsP8. If required permission cannot be checked, it omits the item rather than trusting an old list. - Too many candidates are removed. Bound extra retrieval. Returning fewer posts preserves privacy and the work limit; an unchecked filler post does not.
- Ranking changes during paging. The cursor continues one retained ranking view with a stable post-ID tie-break. If that view expires, tell the client to restart. A seen-ID set can suppress repeats but cannot recover all items skipped by arbitrary reranking.
- Scoring or fanout is slow. Use a cheap chronological order over permitted candidates when scoring times out. When follower-list work is delayed, show older eligible content with a freshness indication and alert on queue age. The committed source post must remain independently recoverable.
09Trade-offs and Validation
Explain the read and write costs using Mina rather than only naming a feed service.
- The ordinary post: the worker prepares
P8once per follower, saving repeated author-timeline reads. - The ten-million-follower page: the handler reads its recent timeline only for active readers, avoiding a huge insert burst for people who never return.
- The privacy rule: both routes must pass the same main-database audience, deletion and follow checks before returning content.
- The operating cost: prepared lists require progress tracking, retry-safe inserts, stale-version rejection and repair. The hybrid also needs merging and a stable page view.
Measure page latency, candidates examined per returned item, age of pending follower updates, publication-to-candidate delay, scoring cost, duplicates, permission failures and fallback use. Engagement features may lag; forbidden content must still be excluded. Evaluate relevance with controlled experiments and satisfaction or harmful-content checks, because extra clicks or time spent do not automatically mean a better feed.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
A model can optionally rank the posts already found for Mina. Finding possible posts and deciding which post may be read remain separate operations.
- Input and output. After checking access, the feed handler sends a bounded set of post IDs with recency, approved engagement counts and consented viewer preferences. The model returns a score for each supplied ID, not new post content. The TensorFlow Recommenders example separates candidate retrieval from this ranking step.
- Integration. The handler orders candidates by those scores, then loads selected display records and checks current versions and permission again. A high score cannot restore
P8after deletion or an audience change. - Fallback. Assume a30 ms scoring budget within the 300 ms page target. If feature loading or inference misses it, use chronological order over currently permitted candidates. Batch feature requests and cap candidates so a model upgrade cannot create unlimited remote calls.
- Evaluation and privacy. Measure relevance, satisfaction, diversity, harmful-content regressions, response time and cost/page. Protect private interaction history and honor deletion and retention in training copies. Keep the model version with the page’s ranking context so an experiment does not silently change later-page ordering.
DRAW. CALCULATE. EXPLAIN.
Choose fanout policies and prove deletion safety
75 minutes · guided practice
Model three authors: 100 followers posting ten times/day, 20,000 followers posting twice/day, and ten million followers posting once/day. Compare eager write counts with pull work for 1% daily active followers. Design the read stages and a stable cursor. Then process create version 8 after delete version 9 and show why the post remains invisible.
Compare the worked solution
- Count prepared inserts. The authors need
100×10 =1,000,20,000×2 =40,000, and10,000,000×1 =10 millionreferences/day respectively if every publication updates every follower’s list. - Count active readers separately. At 1% daily activity, the first author has one active follower, the second has 200, and the third has 100,000. Pull cost also depends on pages per reader and timelines read per page, so these counts do not prove one universal follower threshold.
- Choose a hybrid. Prepare references for ordinary authors. For the huge page, read its recent timeline when an active viewer asks. The handler merges both ID sets, removes duplicates and checks current access before ranking a bounded set and loading display records.
- Continue a defined ranking. Return a cursor referring to a short-lived ranking view and stable post-ID tie-break. If it expires, require a restart instead of silently treating a new order as the old traversal.
- Reject old work after deletion. Retain delete version 9. A worker processing create version 8 compares against it and does not restore
P8. A read also checks the source post and current audience, so candidate cleanup may lag without exposing a deleted body. - Explain each saved record. The post is the current content; the outbox retains unfinished publication work;
(viewerId,postId)prevents duplicate list inserts. None of those list entries is permission to read.
The checkboxes record your own assessment. The worked solution gives you a way to check the reasoning.
RETRIEVE BEFORE YOU REVEAL
Keep the important ideas.
Say the answer aloud, then turn the card over.
CARD 01How do push, pull and hybrid feeds differ?
Pull makes the feed handler read followed authors’ timelines when a viewer asks. Push makes a worker prepare follower references when an author publishes. A hybrid pushes ordinary authors and reads very large authors’ recent posts only for active viewers.
CARD 02What happens between a post ID and a displayed feed item?
The handler collects possible IDs, removes duplicates, checks current permission and scores a bounded set. It then loads display records, called hydration. An old feed entry or high score does not grant permission to return the body.
CARD 03How do deletion and pagination stay coherent?
The main post record retains newer versions and deletion state so a delayed worker cannot restore an old create. Page cursors continue a defined ranking view with an ID tie-break. When that view expires, restart explicitly.
CHECK THE REASONING
Can you explain the choice?
Answer every question, then finish for an immediate score and an explanation for each answer. Aim for 80%, then revisit any gaps.
CLOSE THE LOOP
Explain it without the page.
Summarise the mechanism, one trade-off, and one failure mode. Then mark this chapter complete.