API Design
In one line: the feed endpoint takes a parameter unlike anything in the previous chapters, and it encodes a genuinely different approach to pagination.
The five endpoints
postMedia(userID, media_type, media_file, list_of_hashtags, caption) followUser(userID, target_userID) likePost(userID, post_id) searchPhotos(userID, keyword) viewNewsfeed(userID, generate_timeline)
| Parameter | Description |
|---|---|
media_type | The type of media — photo or video |
media_file | The media file itself |
list_of_hashtags | All hashtags, maximum 30 |
caption | Text, maximum 2,200 characters |
target_userID | The user to be followed |
keyword | Username, hashtag, or place typed in the search bar |
generate_timeline | The time when a user requests feed generation. Instagram shows posts not seen between the last request and this one |
generate_timeline is a timestamp cursor — and it solves the seen-state problem the newsfeed chapter flagged
This is the most interesting parameter in the chapter, and it does something none of the previous feed APIs did.
"It indicates the time when a user requests news feed generation. Instagram shows the posts that are not seen by the user between the last news feed request and the current news feed request."
So the client sends when it last asked, and the server returns everything since. That is a time-range cursor, and it neatly addresses two problems earlier chapters had:
Pagination. The Twitter chapter needed a cursor because offsets break in a growing feed. The newsfeed chapter's getNewsfeed(user_id, count) had none at all. This parameter is a cursor.
Seen-state. The newsfeed chapter flagged that a ranked feed is non-deterministic, so refreshing re-shows content, and nothing stored what a user had already seen. A last-request timestamp is a cheap approximation: everything before it was shown, everything after is new.
But it only works under one condition, and it is worth naming precisely: the feed must be chronological. Lesson 2 noted the requirement says chronological while the rest of the chapter implies ranking. Here is where that matters:
| Chronological feed | Ranked feed | |
|---|---|---|
| "Since timestamp T" | Well-defined — posts newer than T | Meaningless — position is not time |
| Cursor | A timestamp works | Needs a frozen list and an offset into it |
| Seen-state | Implied by the timestamp | Must be stored explicitly |
So generate_timeline is an elegant solution to the chronological problem, and it does not survive the move to ranking. The moment suggested and promoted posts are injected — which the same requirement mentions — the timestamp no longer determines what was shown.
A timestamp cursor is the cheapest possible seen-state, and it is exactly as durable as your commitment to chronological ordering.
postMedia sends the file inline, and at 150 MB that will not work
postMedia(userID, media_type, media_file, ...) puts the media file itself in the request, alongside the metadata.
At Lesson 3's stated sizes — up to 150 MB per video — a single POST carrying the file has real problems: no resumability on a dropped mobile connection, a request timeout long enough to be a denial-of-service risk, and the application server tying up a connection for the duration of the transfer.
The standard resolution is a two-phase upload, and it is worth naming because it changes the architecture:
1. Client asks for an upload URL -> server returns a PRE-SIGNED URL 2. Client uploads DIRECTLY to blob storage (resumable, chunked) 3. Client confirms; server writes metadata and enqueues transcoding
Three benefits. The media never traverses the application tier, which matters at 502.8 Gb/s of ingest. Uploads become resumable, which matters on mobile. And the application server handles a small metadata request rather than holding a connection for a 150 MB transfer.
Large uploads should go directly to storage, not through your application servers. The same instinct as serving media from a CDN rather than origin — keep bulk bytes out of the tier that does per-user work.
likePost takes no author ID, unlike that building block's
Compare the two:
Instagram: likePost(userID, post_id) Twitter: likeTweet(user_id, tweet_id, tweeted_user_id, user_location)
The Twitter chapter carried the author ID and location as routing hints — evidence of geographically sharded counters, letting the write go straight to the right shard without a lookup.
Instagram's signature has neither, so every like requires resolving post_id to its author before the counter can be updated. That is one extra lookup on an operation that Lesson 11 says must handle "millions of interactions on a celebrity post" with sharded counters.
Not wrong — just a design choice with a cost, and one worth surfacing. A minimal API signature can push work onto the server that the client already knew. The client rendering the post already has the author ID; sending it is free.
Thirty hashtags and 2,200 characters are product limits with infrastructure consequences
The design specifies both caps precisely: 30 hashtags, 2,200 characters.
Worth noticing because bounded fields make several things tractable:
Fixed-ish metadata size. Lesson 6's schema puts a photo row at 394 bytes, which is only predictable because the caption is capped.
Bounded index writes. Thirty hashtags means at most thirty inverted-index insertions per post — a known, small fan-out into the search index rather than an unbounded one.
Bounded hashtag-page fan-out. Each hashtag is effectively a feed, so 30 is the cap on how many hashtag timelines one post can enter.
A product limit on content size is an infrastructure constraint in disguise — the same observation that building block made about 280 characters, and it is what makes per-post work predictable.
The two-phase upload, and who confirms it
Creating a post is not one request. Metadata is created first, the bytes arrive separately, and something has to reconcile the two.
uploadStatus is the field that makes this safe. A post row exists before its media does, so the feed must never surface a post whose bytes never arrived — the status flag is what prevents a broken image in someone's feed.
Server-driven confirmation is the more reliable choice. The client-driven PATCH is simpler and trusts the client to report honestly; a blob-store event notification keeps the backend in control. Most production systems accept the extra complexity for that guarantee.
Multipart matters for video. A single HTTP request tops out well below a multi-gigabyte file, so large uploads are split into parts that upload in parallel and retry individually. The presigned URL is what lets those parts go straight to storage rather than through the API tier.
Key takeaway
generate_timeline is a timestamp cursor that doubles as the cheapest possible seen-state — solving two problems earlier chapters left open — but it works only if the feed is chronological, and the same requirement that specifies chronological also injects suggested and promoted posts. postMedia sends the file inline, which at 150 MB should be a two-phase upload with a pre-signed URL so media never traverses the application tier. likePost omits the author ID that Twitter's equivalent carried as a sharding hint, costing a lookup per like. And the 30-hashtag and 2,200-character caps are product limits that make per-post index work bounded and metadata size predictable.
Next: the storage schema, and a 500-fold discrepancy inside it.