Design complete systems / CHAPTER 53
Design a microblogging service (Twitter)
Publish short posts and build follower feeds, using prepared lists for ordinary authors and read-time merging for large audiences.
Builds on Design a photo-sharing service (Instagram), Design a messaging service (Facebook Messenger), Read-heavy versus write-heavy workloads, Push versus pull architecture, Database indexes.
- Keep original posts separate from copied feed IDs and check the current records before display.
- Compare preparing follower lists with gathering authors’ posts when a viewer reads.
- Explain how later pages retain their order, how unfollowing removes posts, and how deletion blocks old feed/search copies.
01Functional requirements
An author timeline lists one author’s posts. A home feed combines posts from authors the viewer follows. Prepared home lists store post IDs; the original post record still decides its text, deleted state and audience.
FR1: Publish and remove posts. Create short posts and replies without duplicating them on request retries. Let authors delete owned content. Media references come from verified photo/video uploads.
FR2: Follow authors. Support following, unfollowing and protected-author access rules. Reposts and search results must still check whether the viewer may see the original post now.
FR3: Read timelines. Serve author timelines and a home feed of followed authors' posts. Start with newest-first ordering. For ranked feeds, later pages continue the same saved post order. If ranking fails, order the posts by creation time instead.
FR4: Record engagement. Let each account set or remove one like per post. Store a repost as a reference to the original. Show permitted replies without exposing an unavailable parent.
FR5: Search post text. Find matching post IDs through a text index, then retrieve their current records and check permission before showing results.
Out of scope
- Direct messaging and advertising.
- Suggestions outside the follow graph. Trending topics, mentions and curated products need their own requirements.
Example: Mira publishes p81. Ben sees it in his feed, replies with p82 and likes or reposts p81. His repost does not create an independent copy that can ignore Mira's later deletion.
02Nonfunctional requirements
Assume 100 million daily active users. Five percent publish twice daily, and each user opens ten home-feed pages per day. Confirm and test these planning assumptions.
NFR1: Feed latency. At least 95% of primary-region home-feed requests finish within 300 ms at the planned peak.
NFR2: Feed availability. Home feeds meet 99.9% availability per calendar month. If optional ranking fails, use a limited newest-first fallback.
NFR3: Publication durability. A successful publish means the post and recoverable work for its later distribution have been saved. Accepted posts survive one storage-node failure under the selected replication policy.
NFR4: Home freshness. At normal load, at least 95% of ordinary-author posts enter eligible active followers' prepared feeds within five seconds. Very large authors use read-time merging instead; neither approach promises instant visibility everywhere.
NFR5: Deletion and audience. Stop returning deleted content in newly generated responses within 60 seconds, including through caches. Check current protected-post permission on indirect reads too. Old screens and already downloaded bytes cannot be recalled.
NFR6: Safe retries and delayed updates. Repeated events create only one recipient/post feed entry. Repeated likes describe one account action. An older update cannot undo a newer deletion. Displayed totals may lag, but must be repairable from the saved actions.
NFR7: Ordering and search delay. Order equal creation times or scores by post ID. A cursor names the saved ordering and last returned position, so the next page continues that browsing session rather than starting a newly ranked list. No global order across all authors is required. Target 30-second search propagation, while current visibility always overrides an old search result.
03Quantify read and fan-out amplification
- Publishing: five million authors × two posts/day = ten million posts/day, about 116 creations/second.
- Feed reads: 100 million users × ten pages/day = one billion requests/day, about 11,600/second. At 20 posts/page, separately fetching every record could mean 232,000 post lookups/second before batching or caches.
- Prepared home lists: if each ordinary post adds a reference for 200 active followers, ten million posts create two billion entries/day. At 32 bytes each, IDs alone need 64 GB/day before indexes, replicas and retention.
- Exceptional authors: one author with ten million followers can create ten million list updates for one post. The average publishing rate hides this burst.
Design decisions
- Bound page size, fetch posts in batches and cache frequently reused records.
- Prepare ordinary-author home lists in background programs, using small references rather than copying full text.
- Compare the cost of a large author's many list writes with merging their recent posts when a follower reads.
04API Design: Requests, Responses and Retries
Identify one publishing request with a request key. If the client retries after a timeout, return the original post instead of creating another:
POST /posts
Idempotency-Key: publish-81
{"text":"Garden opens Saturday.","replyTo":null,"mediaIds":[],"visibility":"public"}
{"postId":"p81","version":1,"state":"published","createdAt":"2026-10-24T12:00:00Z"}
Creation results
201 Created: the post, original request result and pending feed/search work have been saved together.- Same request key and content: return the original
p81result. 409 Conflict: that key was used with different content.400: text length is invalid.403: the account may not publish.
Replies, likes and reposts
- A reply calls
POST /postswith replyTop81. First check that its author may read the parent post. PUT /posts/p81/likes/mesaves the signed-in account's like;DELETEremoves it. Repeating the same operation keeps the same final state.- A repost saves the original post ID
p81rather than a separate copy of its text. Later reads fetch the current original and check its current visibility.
Browse posts
GET /authors/M/posts?cursor=...lists Mira’s posts; M is her account ID.GET /home?cursor=...returns the viewer's home feed.GET /search?q=garden&cursor=...searches posts.- Each page contains post IDs, current display details and a continuation cursor. Finding a post in a feed or search result never bypasses its permission check.
- Ben calls
PUT /authors/M/followto follow Mira andDELETE /authors/M/followto unfollow. The signed-in account determines the follower; repeating the operation leaves the same final relationship state. A protected author must approve the relationship before its posts become readable. - An author approves or revokes a follower with
PUTorDELETE /followers/{accountId}/approval; the handler checks account ownership.DELETE /posts/p81checks Mira owns the post and saves a newer deletion version. A repost request names originalPostIdp81and saves a reference after checking that the caller may read it.
05Data Model and Storage Access Patterns
| Record and lookup key | What it stores and why |
|---|---|
Post, postId; author/time index |
Text, author, parent reply ID, media references, visibility, version and deletion time; fetch one post or one author's recent posts |
Follow, (followerId,authorId); reverse author index |
Current follows; find who follows a publishing author |
Like, unique (postId,accountId) |
One account's current like; repeated requests cannot create additional likes |
Repost, (accountId,originalPostId) |
Original post ID and repost time; fetch current text and visibility when shown |
Home entry, (recipientId,sortKey,postId); unique (recipientId,postId) |
A post ID and its ordering information for one viewer; rebuild this list from posts and follows if needed |
Publication/change record, eventId |
Saved work describing a particular post version; workers update feeds, search and counts from it |
- A displayed count can lag the saved likes. The individual like rows remain the record of who liked the post and allow totals to be rebuilt.
- To load a reply thread efficiently, add an index beginning with parent post ID and reply time. Without it, the database may have to inspect unrelated posts.
06Start with fan-out on read
Start with one database containing posts and follow relationships. A post's ID, such as p81, identifies its saved text and current visibility.
Publish once, including after a timeout
- Mira's client labels her request
publish-81. The application checks whether that account/request key already has a saved result. - If the content matches a previous attempt, return its
p81result. Reject changed content under that key. - For a new request, one transaction saves the post, the request result and pending work for later feed/search updates. A unique request key prevents simultaneous retries from creating duplicates.
Build Ben's first home feed
- Find the authors Ben currently follows.
- Fetch a limited recent list from each author using the author/time index.
- Merge those posts newest first, check current visibility and return the first page.
- This is fan-out-on-read, or pull: gather the posts when Ben reads. It needs no prepared follower home list.
If Ben follows thousands of authors, each page can require many queries and comparisons. Cap source work and measure it. More parallel queries may reduce elapsed waiting while still consuming substantial storage/network capacity. The next stage prepares common results before readers ask.
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 bounded follow graph can use simple indexed source queries before a distributed home index is needed.
Stage 1: chronological fan-out on read
Follow the numbered arrows
- Publishing author → Post / feed applicationPublish / retry request publish-81
- Post / feed application → Post + follow databaseCommit post / query followed authors
- Reading user → Post / feed applicationRead chronological home
- Post / feed application → Reading userCurrent permitted post details
Trace the diagram in words
- Commit p81 once.
- Find Ben's followed authors.
- Merge a bounded page from source author indexes.
- Store one source post and follow relationships.
- Merge recent author lists and apply current visibility for each page.
The cost: Publication is cheap, while every home request pays followed-author query and merge work.
Why change it? Repeated queries across followed authors and merging their results consume too much of the 300 ms response target. Prepared recipient lists remove that discovery work. Fetching full post details still needs batching and cache reuse; its naive estimate remains 232,000 lookups/s.
Stage 2: push IDs, preserve source authority
Follow the numbered arrows
- Publishing author → Post / feed applicationPublish / retry request publish-81
- Post / feed application → Post + follow databaseSave post and pending update work
- Post + follow database → Fan-out workersRecoverable publication event
- Fan-out workers → Recipient home indexRecipient / post inserts
- Reading user → Post / feed applicationRead home with cursor
- Post / feed application → Recipient home indexLoad candidate IDs
- Post / feed application → Post + follow databaseLoad current post; check permission
- Post / feed application → Reading userPermitted ordered page
Trace the diagram in words
- The application saves p81 and pending distribution work in the same transaction.
- A worker inserts Ben/p81 only if absent, so repeating the work cannot add another entry.
- On a home read, the API fetches current posts and removes entries whose author Ben no longer follows or whose content he may no longer view.
- Add workers that resume through follower pages and save candidate post IDs under each recipient’s key.
- Find ordinary-author candidates in a short prepared list, then fetch current post details and check permission.
Inspect the connection changes
- Now: Post / feed application → Post + follow database: Save post and pending update work
- Now: Post + follow database → Fan-out workers: Recoverable publication event
- Now: Fan-out workers → Recipient home index: Recipient / post inserts
- Now: Reading user → Post / feed application: Read home with cursor
- Now: Post / feed application → Recipient home index: Load candidate IDs
- Now: Post / feed application → Post + follow database: Load current post; check permission
- Now: Post / feed application → Reading user: Permitted ordered page
- Previously: Post / feed application → Post + follow database: Commit post / query followed authors
- Previously: Reading user → Post / feed application: Read chronological home
- Previously: Post / feed application → Reading user: Current permitted post details
All other connections are retained. Response paths are implicit unless drawn.
The cost: This saves foreground discovery but creates about two billion daily recipient entries under the ordinary-author assumption; universal push fails on celebrity bursts.
Why change it? A ten-million-follower post creates an unacceptable immediate insertion burst even if average create rate is low.
Stage 3: ordinary push plus bounded large-author pull
Follow the numbered arrows
- Publishing author → Post / feed applicationPublish / retry request publish-81
- Post / feed application → Post + follow databaseSave post and pending update work
- Post + follow database → Fan-out workersOrdinary-author publication event
- Fan-out workers → Recipient home indexRecipient / post inserts
- Reading user → Post / feed applicationRead home with cursor
- Post / feed application → Recipient home indexLoad candidate IDs
- Post / feed application → Post + follow databaseLoad current post; check permission
- Post / feed application → Reading userPermitted ordered page
- Post / feed application → Large-author recent cachePull large-author candidates
- Post + follow database → Large-author recent cacheRebuild / refresh from author index
Trace the diagram in words
- Workers prepare home-list entries for ordinary authors.
- The application fetches a limited recent list for each followed exceptional author.
- It merges the IDs, checks current follow relationships and post visibility, and continues the page’s saved ordering view.
- Change workers to skip designated large authors.
- Add a replaceable recent-author cache and merge those IDs with prepared home candidates.
Inspect the connection changes
- Now: Post + follow database → Fan-out workers: Ordinary-author publication event
- Now: Post / feed application → Large-author recent cache: Pull large-author candidates
- Now: Post + follow database → Large-author recent cache: Rebuild / refresh from author index
- Previously: Post + follow database → Fan-out workers: Recoverable publication event
All other connections are retained. Response paths are implicit unless drawn.
The cost: The hybrid avoids millions of prepared entries for one large author, but some reads must merge author lists and reload their cache. Measure which authors justify that separate path.
07Generate and place IDs without losing timeline access
Choose a post ID
| Method | How it gets uniqueness | Cost |
|---|---|---|
| Database sequence | One issuer assigns distinct integers | Coordination with the issuer |
| Reserved ranges | Each server gets a saved, non-overlapping range | Track assignments; skip unused numbers after crashes |
| Random IDs | Choose a large unpredictable value and handle duplicates | No inherent chronological order |
| Time/worker/sequence | Distinguish time, server and local counter | Correct worker assignment and clock/restart rules |
Understand a toy time-based ID before choosing it
- At time tick 1042, server 07 emits
1042-07-00; server 09 emits1042-09-00. Different server numbers keep these IDs distinct. - Server 07's next ID is
1042-07-01. No two live generators may use the same server number. - If server 07 uses every sequence number for tick 1042, wait for a later tick before resetting the counter. If its clock moves back to 1041, pause rather than issue combinations it may already have used.
- Before issuing IDs for a tick, durably save that tick or a higher reserved limit. After a restart, recover that saved limit and wait until time advances beyond it before reusing worker 07. If that record cannot be recovered or the previous process may still run, obtain a different unused worker number or pause generation. Never assume a restart makes earlier IDs available again.
- This demonstrates the required checks; choose a tested generator that supplies them and document its time/worker/counter allocation. A timestamp alone does not guarantee uniqueness.
Choose record placement separately
- Author-based partitions keep one timeline together but can overload a popular author's partition.
- Hashing
postIdspreads rows, so keep an author/time index to find an author's recent IDs. - Creation-time ranges concentrate new traffic unless further divided. Map logical partitions to machines so selected groups can move deliberately.
- A home list contains IDs. Fetching the current text, audience and media behind those IDs is called hydration. Batch these lookups by partition; a 20-post page should not require 20 independent network round trips.
- Add memory first if adequate, then replicas for public reads that tolerate delay. An author's immediate publication confirmation needs a read known to contain that write.
Worker IDs distinguish otherwise identical time and counter values
Follow the numbered arrows
- Worker 07 at tick 1042 → 1042-07-00Include the assigned worker ID in the post ID
- Worker 09 at tick 1042 → 1042-09-00Same tick, different worker
- 1042-07-00 → Author / time indexStore author access entry
- 1042-09-00 → Author / time indexFind the partition and load current post details
Trace the diagram in words
- Two workers can have the same local sequence and time.
- Worker IDs 07 and 09 keep the two IDs different even when their tick and local counter match.
- Save the tick limit before issuing IDs, wait beyond it after clock recovery, and never assign one worker ID to two live generators.
08Introduce a hybrid home-feed pipeline
Prepare ordinary-author feeds
- Save a pending distribution record with Mira's post. This record is an outbox entry: the database remembers work that still needs to be sent after a commit.
- A relay program sends that work to a queue and retries if a response is lost. A worker reads follower pages and records where it reached.
- For each active follower, insert
(viewerId,postId)only if it is absent. The database's unique pair prevents repeated work from adding another feed entry. - Ben now reads one short prepared list and fetches current post details in batches. Check both that he still follows each author and that he may currently view the post. Old list entries do not preserve a follow relationship. Store a limited recent window grouped by recipient.
- This is fan-out-on-write, or push: publication schedules many list writes so later reads do less work.
Handle very large authors on reads
- Copying one post to millions of followers can dominate background capacity. In the final stage, skip prepared-list insertion for designated large authors.
- Keep their recent author IDs, optionally cached. On a cache miss, reload the author/time index.
- Merge a limited recent set into Ben's prepared list when he reads, then load and check current records before showing text.
- Estimate required feed insertions as posts/second × active followers, then compare it with tested worker capacity and the freshness target. For example, ten million inserts at an assumed 25,000 inserts/second take at least 400 seconds before overhead. Merging that author’s posts when followers read avoids that publication burst, but adds work to those reads.
Recovery costs
- Saved outbox work survives a crash between publication and queue delivery.
- Worker checkpoints let follower-page processing resume.
- Unique recipient/post pairs make repeated insertions harmless.
- Candidate caches find possible posts; they never grant current permission.
09Keep engagement and search off the home-feed authority
Avoid copying full bodies into every feed
- Two billion daily references × 32 bytes = 64 GB before overhead.
- Copying a 1 KB body for every entry would take about 2 TB/day.
- IDs plus a shared post cache save space, but every response still loads current state and checks permission.
Apply search changes in version order
- An inverted index maps words to post IDs.
- Index updates carry the original post version. Keep a deletion marker with its version after removing a searchable body.
- If deletion version 2 arrives before delayed creation version 1, ignore version 1. Arrival order must not restore deleted text.
Count likes from saved account actions
- Store one like state for each
(postId,accountId). Repeated PUT requests set that same state instead of adding votes. - Give changes IDs/versions. A count-updating worker remembers processed changes and applies each once, using a transaction for its processed marker and total update.
- Rebuild displayed totals from saved like rows if necessary. Exact immediate updates on a viral post can become a write bottleneck; delayed or approximate totals are options only if the product permits them.
Choose the feed trade-off
- Pull saves work for inactive readers but gathers many authors when they return.
- Push makes common reads cheaper but writes lists for people who may never read them.
- The hybrid bounds exceptional large-author work. Measure queue depth and oldest unfinished work, and retain a limited chronological fallback if optional ranking fails.
10Trace posts, reads and follow changes
- Publish: Mira submits
publish-81. One transaction saves postp81, its request result and pending evente81for follow-up work. Heree81labels that work, not a second post. - Prepare: a worker inserts
(Ben,p81)only if absent. Replayinge81cannot duplicate Ben's list entry. - Search: another worker adds
p81's words to the text index. Search visibility has its own processing delay. - Read: Ben’s feed handler combines prepared IDs with large-author IDs, fetches current records and checks current follows and post visibility before returning a stable page. The saved list controls the order of later pages; it grants no continuing access after Ben unfollows an author.
- Like: repeated
PUT /posts/p81/likes/mekeeps one Ben/p81 action. A background program counts each identified change once and can repair totals from saved actions. - Unfollow: Ben removes his follow relationship to Mira. A prepared entry for
p81may remain, and a delayed worker may insert it again. The next home read checks the current relationship and omits it; background cleanup later removes the stale entry. Directly opening a public author page follows that page’s access rules instead. - Delete: Mira saves deletion version 2. Old creation version 1 cannot undo it. A repost still refers to
p81and must check that original’s current state.
Continue pages predictably
- For a newest-first page, order by creation time and then post ID. Capture a maximum creation time on the first page, and include that limit plus the last returned time/ID in the cursor. Later pages exclude posts with creation times after that limit and continue after the last returned pair. A later database insert can carry an older timestamp, so this does not freeze which posts exist. If fixed membership is required, save the ordered IDs as in the ranked-page approach below.
- For ranked pages, save the ordered candidate post IDs under a view ID with an expiry. The cursor contains that view ID and the last returned position. Later pages use the saved order while still rechecking follows and visibility. If the view expired, ask the client to restart.
- Remembering already-shown IDs prevents some repeats, but cannot recover posts skipped because the service recalculated a different order between requests.
11Plan failure, moderation and cost
- Distribution is late: author pages can read saved posts while home lists lag. Monitor the oldest unfinished follower batch; a short queue can still contain very old work.
- Fallback overload: cap read-time author queries. Replacing every failed prepared-feed request with thousands of database reads can spread the outage.
- Deletion has not reached every copy: save deleted state in the original, remove cached bodies within the promise and clean discovery lists in the background. Check current state before display, including through reposts.
- Retries: retain original request results and unique list pairs. Queue redelivery is expected after failures; it must not create another post or list entry.
- Abuse and monitoring: limit rapid posting/follow changes, isolate abusive accounts and avoid sensitive log tokens. Measure publish/feed latency, duplicate attempts, distribution age and cache reuse.
- Defend the cost: prepared lists spend storage and background writes to save repeated read work. Large-author merging limits that cost while preserving current permission checks.
OPTIONAL DESIGN EXTENSIONWhere AI Fits in This Design
Use: rank posts the viewer is already allowed to see
- Input: a limited set of posts the viewer may see, their creation times, and prior likes or replies the viewer has agreed may be used for recommendations.
- Output: scores for those same posts. The model cannot add a forbidden/deleted post or treat a repost as permission.
- Integration: gather candidate IDs, check current visibility and rank within an assumed 50 ms slice of the 300 ms page target. Recheck visibility before display.
- Pagination: save the model version and ordered candidate IDs under a view ID with an expiry. Later-page cursors use that ID and the last returned position, rather than rerunning the model and shuffling the order.
- Fallback: if scoring exceeds 50 ms or the interaction history cannot be loaded, order the permitted candidates by creation time, newest first. Model failure must not stop publication, deletion or likes.
- Evaluation and limits: test recommendations against interactions not used for training, then try the model with a small group before expanding. Check whether it repeatedly shows one author, promotes clickbait or uses outdated interaction data. Measure user feedback, cost per page and slow requests. Set a deletion period for stored private interaction history.
TensorFlow Ranking documents model-based list scoring. The product still chooses the objective; more clicks alone do not define a useful feed.
DRAW. CALCULATE. EXPLAIN.
Choose fan-out policy with an explicit write budget
75 minutes · guided practice
Mira publishes p81 with ID components time tick 1042, worker 07 and local sequence 00. Worker 09 also uses sequence 00 at that tick. Ben retries the same like three times and reposts p81. A delayed publication record arrives after deletion version 2.
- Explain why the generated IDs differ and why Ben’s retries create only one like.
- Trace the saved post, feed/search copies and deletion; show how old work is rejected.
- For an author with ten million followers and a hypothetical budget of 25,000 follower-feed inserts/s, calculate the ideal completion time and choose push, pull or hybrid delivery.
- Compare raw storage for two billion 32-byte post references with two billion copied 1 KB bodies.
Compare the worked solution
- The toy IDs 1042-07-00 and 1042-09-00 differ by their uniquely assigned worker IDs. Keep worker ownership, counter limits and clock/restart rules explicit; a database sequence is a simpler starting alternative.
- Retrying
publish-81returnsp81. Repeated like PUT requests keep one Ben/p81 row, and identified changes increase derived totals only once. - Save distribution work with the original post. Workers remember their follower-page progress and insert each viewer/post pair only if absent.
- Deletion version 2 overrides delayed creation version 1. Reposts and search still check the original’s current state before showing text.
- Two billion × 32 bytes = 64 GB/day; copying 1 KB each takes about 2 TB/day. References still require current-record lookups and permission checks.
- Ten million insertions ÷ 25,000/second = at least 400 seconds, or 6 minutes 40 seconds, before overhead. Prepare ordinary-author lists, but merge this exceptional author’s recent posts during reads instead of promising five-second mass distribution.
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 does a post reach several product views?
- Save the original post, retry result and pending feed/search work.
- Workers copy its ID into discovery lists.
- Home reads check current follows, post state and audience.
- A repost cannot bypass original deletion.
CARD 02What does an ID generator need beyond a timestamp?
A timestamp alone is insufficient.
- Give simultaneous generators distinct worker IDs or number ranges.
- Wait for the next tick when that tick’s counter is full.
- Pause after a backward clock move.
- Before issuing IDs, save the tick upper bound. After a restart, recover it and wait beyond it, or obtain a different unused worker ID.
- Keep a separate author/time index for listing posts.
CARD 03Why fan out IDs and keep source actions?
- Prepared feeds store IDs to avoid copying every body.
- Reads fetch current text and permission.
- One saved account/post like makes retries harmless and totals repairable.
- Merge very large authors on reads to limit list writes.
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.