Free preview

API Design

In one line: two calls, and the difference between them is the design. One is invoked by the system, the other by the user, and everything about their requirements diverges.

Generate the newsfeed

generateNewsfeed(user_id)

This API accepts a user ID and retrieves the user's friends and followers. It then composes a newsfeed containing ranked candidate posts. Because this API is invoked by internal components, it can be executed asynchronously to precompute user feeds. The precomputed feeds are stored in persistent storage and cached for fast retrieval.

An API with no caller in the client — and that is the point

generateNewsfeed is never called by a phone. The design is explicit: "invoked by internal components."

That makes it a job, not an endpoint, and its requirements are the inverse of a user-facing call:

generateNewsfeedAn ordinary API
CallerInternal — a fan-out worker or schedulerThe client
Latency budgetNoneStrict
Failure handlingRetry later; nobody is waitingReturn an error
InvocationTriggered by a post, or by a cold readPer user action
Idempotent?Must be — retries will happenShould be

That last row matters and the signature does not address it. If the generation job runs twice for the same user, the feed must not end up duplicated. Since it regenerates the feed rather than appending to it, that holds naturally — a job that computes a value from scratch is idempotent for free, unlike one that mutates incrementally.

Putting a job behind an API signature is a modelling choice worth questioning. It reads as though a client might call it, and clients must not — an exposed generateNewsfeed would be a trivial denial-of-service vector, since it is the single most expensive operation in the system.

One parameter, and what it does not include

user_id alone. Which tells you the generation service reads everything else from storage: the follow graph, the candidate posts, the user's history for ranking.

That is right — passing candidates in would couple the caller to the ranking logic — but it means the parameter list hides a real question: how far back does generation look?

A feed generated at 9 a.m. and again at 9 p.m. should not reproduce the same posts. So generation needs either a cursor (everything since the last generation) or a window (the last N hours), and neither is in the signature.

Lesson 8's step 3 says the service retrieves "the latest, most popular, and relevant posts" — so a recency window exists implicitly. Making it a parameter would let the same job serve both incremental updates (new posts since last run) and full rebuilds (a lapsed user returning), which Lesson 4 established are two different paths through the same code.

Get the newsfeed

getNewsfeed(user_id, count)
ParameterDescription
user_idThe user for whom the system will fetch the newsfeed
countThe number of feed items retrieved per request

The getNewsfeed API returns a JSON object containing a list of posts.

No cursor — so this API cannot paginate correctly

count says how many. Nothing says from where.

The Twitter chapter established why that fails: in a feed that is constantly growing, an implicit "from the top" or an offset breaks as soon as new items arrive between requests. The user sees duplicates, or misses items, or both.

And Lesson 8's own description requires pagination: "whenever Alice reaches the end of her timeline, the next top N posts are fetched to her screen." That is infinite scroll — which is exactly the case that needs a stable cursor.

The fix is the one that building block used:

getNewsfeed(user_id, count)                  <- as specified: unstable
getNewsfeed(user_id, count, cursor)          <- stable

where the cursor encodes a position in the generated feed, not in time.

That last distinction matters more here than it did for Twitter. Twitter's timeline was chronological, so a time-sortable tweet ID was a natural cursor. A ranked feed has no natural sort key — position 50 is wherever the ranking put it, and re-ranking between requests would reshuffle everything.

Which forces a specific design: the generated feed must be frozen for the duration of a scroll session. You rank once, store the ordered list, and paginate through that — regenerating mid-scroll would make pagination incoherent.

A ranked feed must be materialized before it can be paginated. That is a real constraint the API does not express, and it is one of the strongest arguments for precomputation over pure on-demand generation.

Two calls, and the architecture in miniature

Step back and the pair encodes the whole design:

generateNewsfeed(user_id)          ASYNCHRONOUS, expensive, internal
        |
        v
   [newsfeed cache]                 the materialized, ranked list
        |
        v
getNewsfeed(user_id, count)        SYNCHRONOUS, cheap, user-facing

The cache between them is what decouples the latency budgets, exactly as Lesson 5 described for the generation/publishing split.

And it explains why Lesson 2's requirement — "complex feed generation can run asynchronously, but the read path must remain fast" — is satisfiable at all. The two APIs exist so that the expensive one never appears in a user's latency budget.

Compare a design with one call, getNewsfeed, that generated on demand. It would be simpler and it would put graph traversal, candidate fetching, and ML ranking inside a 2-second window, 58,000 times a second. Every materialized-view architecture is fundamentally this pair of operations — one to build, one to read — and naming it that way is more useful than naming the services.

Key takeaway

generateNewsfeed is a job wearing an API signature — internal, asynchronous, with no latency budget and an implicit requirement to be idempotent, which it gets free by recomputing rather than appending. It should never be client-callable, since it is the most expensive operation in the system. getNewsfeed lacks a cursor, so it cannot paginate a growing feed — and the fix is harder here than for a chronological timeline because a ranked feed has no natural sort key, which forces the feed to be frozen for the duration of a scroll session. Together the two calls are the materialized-view pattern: one operation builds, one reads, and the cache between them is what keeps the expensive half out of the user's latency budget.

Next: the storage schema, including a graph modelled in relational tables.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue