API Design
In one line: two parameters here — next_token and exclude — leak information about the storage design that the architecture lessons never state directly.
Post a tweet
postTweet(user_id, access_type, tweet_type, content, tweet_length,
media_field, post_time, tweet_location,
list_of_used_hashtags, list_of_tagged_people)
| Parameter | Description |
|---|---|
user_id | The unique ID of the user who posted the tweet |
access_type | Whether the tweet is protected (visible only to followers) or public |
tweet_type | Text-based, video-clip, image(s), or a combination |
content | The tweet's actual text |
tweet_length | Text length. For video, the duration and size |
media_field | The type of media delivered — image, video, GIF |
Note: Twitter uses the Snowflake service to generate unique IDs for tweets.
access_type is one word that changes the entire fan-out design
"Whether the tweet is protected (that is, only visible to followers) or public."
That single flag makes timeline delivery a permission-aware operation, and the consequences are larger than they look.
A public tweet can be delivered to anyone — copied into timelines, cached at the edge, served from a CDN, indexed for global search. There is no per-reader check.
A protected tweet must be filtered by the follow relation on every delivery. Which forces a choice:
Check at write time. Fan out only to current followers. Cheap on read, but wrong later — someone who unfollows keeps the tweet in their cached timeline.
Check at read time. Always correct, but every timeline read now consults the follow graph, and Lesson 6's FlockDB is on the critical path of every request.
Real systems do both: fan out to followers at write time and re-verify on read for protected content, accepting the double cost for the small fraction of accounts that are protected.
Any per-reader visibility rule turns a broadcast into a filtered broadcast, and filtered broadcasts cannot be cached at the edge — which is why public content dominates the CDN story in Lesson 3 and protected content does not appear in it at all.
list_of_tagged_people exists so the system can notify — a write that triggers writes
The design answers its own question here: "The system has to notify the people tagged in the tweet."
Worth noting because it adds to Lesson 3's amplification. Posting one tweet triggers:
tweet stored -> fan out to followers -> index for search
-> notify tagged users -> publish analytics event
-> extract hashtags for trend counting
Six downstream effects from one user action. A social write is rarely one write, and that is what the pub-sub block in the building-block list exists to absorb — each of those is a subscriber to a TWEET_POSTED event rather than a synchronous call in the request path.
Which is exactly what Lesson 2's "complex feed generation can run asynchronously" licenses.
The 280-character limit is a design constraint, not a product quirk
The design notes: "A tweet is limited to 280 characters. Hashtags, links, and plain text all count toward this limit."
That bound is load-bearing for the architecture:
- It makes tweet records fixed-ish size — Lesson 3's 250 bytes of text and metadata — which makes storage and cache sizing predictable.
- It makes tweets small enough that per-object cache metadata is material, which is Lesson 8's entire subject.
- It means a timeline of 50 tweets is a few kilobytes of text, so fan-out by value (copying the tweet into timelines) is affordable in a way it would not be for long-form content.
A product constraint on content size is an infrastructure constraint in disguise. Twitter's fan-out design is viable partly because a tweet is 280 characters.
Engagement and replies
likeTweet(user_id, tweet_id, tweeted_user_id, user_location) replyTweet(user_id, tweet_id, tweeted_user_id, reply_type, reply_content, reply_length)
Why does likeTweet need tweeted_user_id and user_location?
Both are redundant on the face of it — tweet_id determines the author, and the server knows the caller.
They are there for sharding and locality, and saying so is a strong interview observation.
tweeted_user_id lets the like be routed without first looking up who wrote the tweet. If engagement counters are partitioned by author — which Lesson 10's sharded counters imply — then including the author ID means the write goes straight to the right shard. Denormalizing a foreign key into the request avoids a lookup on the critical path.
user_location is what makes Lesson 10's "sharded counters improve performance by placing counter shards close to users, similar to a CDN" possible. The like increments a nearby regional shard, and the total is aggregated later.
So these two parameters are the API-level evidence of a geographically sharded, eventually-consistent counter — which is the design Lesson 10 describes. Redundant parameters usually exist to avoid a lookup or to route a write.
Search
searchTweet(user_id, search_term, max_result, exclude, media_field,
expansions, sort_order, next_token, user_location)
| Parameter | Description |
|---|---|
search_term | The search keyword or phrase |
max_result | Tweets per response page. Default 10 |
exclude | What to exclude — replies and retweets. Max 3,200 returned tweets, reduced to 800 when replies are excluded |
expansions | Request additional objects — mentioned users, referenced tweets, attached media and places |
sort_order | Default is most recent first |
next_token | Cursor for the next page. The last page has no next_token |
next_token is a cursor, not an offset — and that is forced by the data
This is the most informative parameter in the API.
An offset ("give me results 100–200") breaks in a stream that is constantly growing. Between page one and page two, thousands of new tweets arrive, everything shifts, and the user sees duplicates or misses results.
A cursor encodes a position in the result set — in practice a tweet ID, which Lesson 2 established is time-sortable thanks to Snowflake. Page two means "everything older than this ID," which is stable no matter how many tweets arrive.
OFFSET: results 100-200 -> shifts as new tweets arrive CURSOR: older than tweet 1234... -> stable forever
So next_token plus Snowflake IDs plus sort_order defaulting to newest-first are one design, not three parameters. The time-sortable ID is what makes cursor pagination possible, and cursor pagination is what makes a growing feed paginable at all.
In any append-heavy feed, pagination must be cursor-based, and the cursor should be the sort key itself.
The 3,200 and 800 limits reveal the index, and the sample response is missing
"The maximum limit on returned tweets is 3,200, but when we exclude replies, the maximum limit is reduced to 800."
Two oddities. First, a hard cap on total results is unusual for search — it exists because the index is bounded, not because the query is. Lesson 7 explains: recent tweets live in a RAM index and older ones in a batch-processed disk index, so deep pagination eventually leaves the fast path.
Second, the limit dropping when you exclude replies is counter-intuitive — filtering should not reduce the ceiling. The likely explanation is that the cap applies to records scanned, not records returned: excluding replies means scanning further to find 800 non-reply tweets, so the scan budget binds sooner.
A result cap that changes with the filter is a scan budget, not a result limit.
The sample response is missing from the design
The design says "Below is a sample JSON response. id represents the posting user, and text contains the tweet content. result_count reflects the number of returned tweets, limited by max_result in the request."
No JSON follows. Reconstructed from the field descriptions given, it would be:
{
"data": [
{ "id": "1234567890123456789", "text": "..." },
{ "id": "1234567890123456790", "text": "..." }
],
"meta": {
"result_count": 2,
"next_token": "b26v89c19zqg8o3fpds..."
}
}
Note the description says "id represents the posting user" — which is likely an error, since in the Twitter API data[].id is the tweet ID and the author appears as author_id. Treat the field naming as approximate.
Timeline, follow, retweet
viewHome_timeline(user_id, tweets_count, max_result, exclude, next_token, user_location) followAccount(account_id, followed_account_id) retweet(user_id, tweet_id, retweet_user_id)
We exclude user_location to retrieve the user timeline. The server also sends a paginated list_of_followers to reduce client latency.
user_location in a timeline call is for ad targeting, and the design says so
The design asks which parameter determines promoted tweets and answers: user_location. "The user belongs to New York, so most probably it gets promoted ads associated with that region."
Worth noticing for what it implies structurally: the home timeline is not purely a merge of followed accounts. It is a merge, plus injected promoted content selected by a different system on different criteria.
That has a real consequence for Lesson 5's fan-out. If timelines are precomputed at write time, promoted tweets cannot be baked in — they are selected per-request, targeted, and auctioned. So even a fully precomputed timeline needs a read-time merge step that splices in ads.
A precomputed feed still has a read-time assembly stage, and once that stage exists it can also handle ranking, filtering deleted tweets (Lesson 2's tombstones), and applying protected-account visibility. The existence of ads forces the very step that makes everything else tractable.
Key takeaway
access_type turns broadcast into filtered broadcast, which cannot be cached at the edge and forces the follow graph onto the read path for protected accounts. tweeted_user_id and user_location on a like are routing hints — API-level evidence of geographically sharded counters. next_token is a cursor, not an offset, which is only possible because Snowflake IDs are time-sortable — in an append-heavy feed, pagination must be cursor-based and the cursor should be the sort key. The 3,200/800 caps are a scan budget, revealing a bounded index. And user_location on the timeline call implies ads, which forces a read-time assembly stage even when the timeline is precomputed — a stage that then also handles ranking, tombstones, and visibility.
Next: the lesson the design omits — how a tweet reaches millions of timelines.