← System design

Social & Feed

Design Recommendation Algorithm

Recommendation looks like a machine learning question, but most of the work is systems work: narrowing 40 million items to a few hundred fast enough that a careful model can rank them inside one page load. Then the design has to keep learning from what users do without ever training on a different picture of the world than the one it serves.
Core: retrieval funnel then precise ranking50,000 requests a second at peakLatency budget 150 msLearns from its own logs

01

The problem

01

The problem

Open almost any large app and the first screen is a row of things picked for you: products, videos, songs, articles. Behind that row is a system that must choose roughly 30 items out of 40 million for one specific person, in less time than it takes the page to paint, and then learn from whatever that person does next.

No model can score 40 million items per request at that speed. So the real design is a funnel. Cheap methods pull a few thousand plausible candidates from several sources, a heavier model scores those carefully, and a final pass adjusts the list for variety, freshness and business rules. Around the funnel sit the pieces that keep it honest: an event stream, a feature store, training pipelines and an experimentation platform.

The question to answer

How do you return a personalised, varied list within 150 ms at the 99th percentile, for hundreds of millions of users, while the catalogue changes daily and the model has to react to what someone clicked a minute ago?

02

What it has to do

02

What it has to do

Functional
  • Return a ranked list for a given user and surface, such as the home page, a "more like this" shelf or a post-purchase email.
  • React within the session: a click now should shape the next page load, not tomorrow's.
  • Handle new users with no history and new items with no interactions.
  • Respect "not interested", hide items already bought or finished, and apply filters such as region availability and age ratings.
  • Let product teams run A/B tests on models, candidate sources and re-ranking rules.
  • Record every impression and interaction for training and measurement.
Non-Functional
  • End-to-end latency of p99 under 150 ms from request to ranked list.
  • Availability of 99.95%. When personalisation fails, a sensible non-personal fallback is served instead of an empty row.
  • Real-time features no more than about 30 seconds behind the event that caused them.
  • The features a model sees at serving time must match the ones it trained on.
  • Cost per thousand requests tracked and bounded, since ranking runs on accelerators.

Goals that pull against each other

GoalWhat it costsHow the design balances it
Relevancebigger models, more features, more latencyheavy model only on the last few hundred candidates
Freshnessstreaming infrastructure and frequent index rebuildssession features in real time, embeddings daily
Varietysome loss in predicted click ratea re-ranking step with explicit diversity rules
Explorationshowing items the model is unsure abouta small, measured slot budget for new items
Ask before you draw

Find out what success means: clicks, watch time, purchases or long-term retention. The target decides your labels, and optimising for clicks alone famously rewards clickbait. Also ask how large the catalogue is, how fast it turns over, and whether there is a search box, since search history is often the strongest signal you will get.

03

Sizing it

03

Sizing it

Take a shopping and media app with 180 million daily users, a catalogue of 40 million items, and about eight recommendation calls per user per day across all surfaces. Each call returns 30 items from a funnel that ranks around 400.

QuantityWorkingResult
Recommendation requests per day180M x 8about 1.44B
Average request rate1.44B / 86,400 sabout 16,700 / s
Peak request rate3x average in the eveningabout 50,000 / s
Items scored by the heavy ranker50,000 x 400 at peak20M scores / s
Ranking acceleratorsabout 300K scores / s per GPU, plus 50% headroomroughly 100 GPUs
Interaction eventsimpressions dominate, about 120 per user per dayabout 22B a day, 250K / s average
Item embeddings40M x 128 dims x 4 Babout 20 GB, about 1.3 GB with 32-byte PQ codes
User embeddings500M monthly users x 128 dims x 2 Babout 128 GB
Online feature storeabout 2 KB per user, 1 KB per itemabout 1 TB plus 40 GB
Training data22B events x about 300 B with features attachedabout 6.6 TB a day
Why a funnel is not optional

Scoring the whole catalogue with the heavy model would mean 40 million inferences per request, two trillion per second at peak. Retrieval cuts that by five orders of magnitude before any expensive model runs, and that cut is what the rest of the architecture is built around.

04

Endpoints and event logging

04

Endpoints and event logging

GET/v2/feed/picks?surface=home&limit=30&cursor=Ranked items for the signed-in user
// user comes from the session; context travels in headers and query // X-Device: android, X-Locale: en-IN, X-Session: s_77b1 // 200 OK { "request_id": "rq_01J9Z4K3", // attached to every later event for attribution "model": "ranker_v41@exp_home_div_b", "items": [ { "id": "sku_884120", "slot": 0, "source": "two_tower" }, { "id": "sku_120931", "slot": 1, "source": "co_view" } ], "next_cursor": "c2xvdDozMA", "fallback": false // true when served from the popular list }
POST/v1/events:batchImpressions and interactions from clients
{ "events": [ { "type": "impression", "item": "sku_884120", "request_id": "rq_01J9Z4K3", "slot": 0, "visible_ms": 1400 }, { "type": "add_to_cart", "item": "sku_884120", "request_id": "rq_01J9Z4K3", "ts": 1759312201 } ] } // 202 Accepted; events land on Kafka, nothing waits on processing
POST/v1/feedbackExplicit negative signal
{ "item": "sku_120931", "reason": "not_interested", "scope": "brand" } // written straight to the user's online profile so the next request already excludes it
Log what was shown, not just what was clicked

An impression with its slot, request id and model version is what turns raw clicks into training labels. Without impressions you cannot tell "saw it and ignored it" from "never saw it", and every model trained on that data learns the wrong lesson.

05

Architecture overview

05

Architecture overview

Online serving path and the loops that feed itDiagram
flowchart LR
	U["App or web client"] --> API["Recs API"]
	API --> CG["Candidate generators"]
	CG --> ANN[("ANN index of item vectors")]
	CG --> CO[("Co-visitation tables")]
	API --> FS[("Online feature store")]
	API --> RK["Ranking service on GPUs"]
	RK --> RR["Re-ranker: rules and diversity"]
	U -. "events" .-> K[["Kafka: interactions"]]
	K --> SP["Flink feature jobs"]
	SP --> FS
	K --> LK[("Data lake")]
	LK --> TR["Training and index builds"]
	TR --> ANN
	TR --> RK

Requests flow left to right through four steps. The recs API loads the user's features and session state, then calls several candidate generators in parallel, each with its own 25 ms deadline. Their results are merged, deduplicated and filtered against what the user has already bought or dismissed. A light pre-ranker trims the union to about 400, the ranking service scores those with the full model, and the re-ranker turns scores into a final page.

Two loops keep the path current. A fast loop runs through Kafka and Flink, updating session counters and recent-item lists in the feature store within seconds. A slow loop runs through the data lake, retraining models and rebuilding the ANN index on a schedule, then shipping new artefacts behind an experiment flag.

Where the 150 ms goes

StepBudget at p99What happens if it is late
Load user features and session15 msrank with default features
Candidate generators, in parallel25 msdrop the slow source for this request
Merge, filter, pre-rank10 msrarely late, runs in process
Fetch item features for 400 candidates15 msuse cached item features
GPU ranking with batching35 msreturn the pre-ranker order
Re-rank and serialise10 msrarely late
Network and slack40 msabsorbs retries and GC pauses

Writing the budget down early matters because each team will otherwise spend the slack on its own component. A rule that every call carries its remaining deadline, and every service gives up when it runs out, keeps one slow dependency from eating the whole request.

Feature store

One definition of each feature with two backends: a low-latency KV store for serving and a warehouse table for training. Using the same code to compute both is what prevents skew.

Ranking service

Batches candidates from many concurrent requests into one GPU call. A dynamic batcher with a 5 ms window raises throughput several times while keeping tail latency inside budget.

Fallback lists

Popular and trending items per region and category, precomputed every few minutes and held in memory on every API node, so a failure upstream still returns a full, reasonable row.

06

Deep dive: retrieval and ranking

06

Deep dive: retrieval and ranking

The funnel for one requestDiagram
flowchart TD
	CAT["40M items in catalogue"] --> R1["Two-tower ANN: 800"]
	CAT --> R2["Co-viewed with recent items: 500"]
	CAT --> R3["Content similarity for new items: 150"]
	CAT --> R4["Trending in region: 100"]
	R1 --> M["Merge, dedupe, filter: about 1,300"]
	R2 --> M
	R3 --> M
	R4 --> M
	M --> PR["Light pre-ranker: 400"]
	PR --> HR["Multi-task ranker: scores 400"]
	HR --> RE["Re-rank for variety and rules"]
	RE --> OUT["30 items on the page"]
Each retriever covers a different blind spot

No single retrieval method is good at everything. Production systems run several in parallel and let the ranker decide between them. The interesting interview question is which gap each one fills.

Co-visitation: people who looked at this also looked at

Item-to-item collaborative filtering counts how often two items appear in the same session, normalised so that globally popular items do not pair with everything. A nightly job builds, for each item, a list of its top 200 neighbours. At request time you take the user's last ten interactions, look up their neighbour lists and sum the weights. It is cheap, explainable and very strong for "next item" intent, but useless for an item nobody has touched yet.

Content similarity for the cold start

For a product listed an hour ago there are no interactions, only a title, images, category and price. An encoder turns those into a vector, and the item becomes retrievable by similarity to things the user liked. Content retrieval rarely wins on its own, but without it new items would never get the first impressions they need to enter the collaborative signals.

Matrix factorisation and the two-tower model

Classic matrix factorisation learns one vector per user and one per item so that their dot product predicts interaction. It works, but a user who signed up today has no learned vector, and it cannot use context such as time of day or device.

The two-tower model fixes both. One neural network, the user tower, takes recent history, demographics and context and outputs a 128-dimensional vector. The item tower does the same from item attributes and IDs. Training pushes the vectors of real interactions together and sampled non-interactions apart, usually with in-batch negatives and a correction for popularity so the model does not just learn "show bestsellers". Because the towers never see each other's inputs, item vectors can be computed offline for the whole catalogue, and only the user tower runs per request.

Two-tower retrievalDiagram
flowchart LR
	UH["Recent history, context, profile"] --> UT["User tower"]
	UT --> UV["User vector, 128 dims"]
	IA["Item IDs, text, image, category"] --> IT["Item tower"]
	IT --> IV["Item vectors, built nightly"]
	IV --> IDX[("ANN index")]
	UV --> Q["Nearest-neighbour query"]
	IDX --> Q
	Q --> C["Top 800 candidates"]

Inside the ANN index

Exact nearest-neighbour search over 40 million vectors is about 5 billion multiply-adds per query. Approximate indexes trade a little recall for orders of magnitude in speed. Two families dominate:

  • IVF with product quantization. K-means splits the space into, say, 16,384 clusters. A query visits the 64 closest clusters only, and each vector inside is stored as a 32-byte compressed code whose distances are computed with lookup tables. The whole index fits in about 2 GB of memory, so every serving node holds a full copy.
  • HNSW graphs. A layered proximity graph walked greedily from coarse to fine. Recall is higher at the same latency, but memory use is several times larger and incremental deletes are awkward.

Tune for recall at 800 above about 95% against exact search, measured on a held-out set. The ranker can only reorder what retrieval gives it, so lost recall here is invisible downstream and very expensive.

The ranker

The ranking model sees each user and candidate pair together, so it can use cross features retrieval cannot: how often this user bought from this brand, the price relative to their usual spend, whether the item is in their cart. A typical shape is a deep network with feature-crossing layers and several output heads, predicting click, add to cart, purchase and "not interested" at once.

The final score is a weighted blend, for example 0.1 x p(click) + 0.4 x p(cart) + 1.0 x p(buy) - 2.0 x p(dismiss). Those weights are product decisions tuned by A/B tests, not by the model. Keeping the heads separate means you can change the blend without retraining.

Re-ranking

A list sorted purely by score tends to show eight near-identical phone cases in a row. The re-ranker walks down the scored list and applies penalties for repeating a category or seller, inserts one or two exploration slots for new items, enforces sponsored placements and policy filters, and removes anything shown to this user in their last few sessions without a click. It is where business rules belong, so they never leak into model training.

07

Design decisions and trade-offs

07

Design decisions and trade-offs

Chosen
Multi-stage funnel

Cheap recall first, expensive precision last. Each stage can be improved and tested on its own, and the GPU bill scales with 400 items per request instead of the catalogue.

Standard at this scale
Rejected
One model over the full catalogue

Fine for a catalogue of a few thousand items. At 40 million it is computationally impossible within any latency budget, however good the hardware.

Does not scale
Chosen
Batch embeddings plus streaming session features

Embeddings and co-visitation tables rebuild daily, while recent clicks, cart contents and session counters update through Flink in seconds. The page responds to what you just did without retraining anything.

Fresh where it matters
Simpler option
Batch only

One pipeline, much less to operate. But a user who browses running shoes for ten minutes keeps seeing yesterday's interests until the next run, which is the moment the product feels dumbest.

Acceptable for email digests
Chosen
Two towers for retrieval, crosses for ranking

Separate towers make item vectors precomputable and searchable. The ranker, which only sees a few hundred items, can afford full user and item interaction features.

Right model per stage
Rejected for retrieval
Cross-feature model everywhere

More accurate per pair, but every user and item combination needs a fresh forward pass, so nothing can be indexed. It belongs in the last stage only.

Too slow for recall
Chosen
Frequent incremental retraining

Warm-start the ranker from the previous checkpoint every few hours on the newest data, and fully retrain weekly. Each candidate model passes offline metrics and a small canary before it takes traffic.

Controlled freshness
Higher risk
True online learning

Updating weights from each event adapts fastest, but one bot farm or logging bug can pull the live model off course in minutes, with no checkpoint review in between.

Only with strong guardrails
Chosen
Compute on request, cache briefly

Run the funnel per request and cache the result for a few minutes per user and surface, so back-and-forth navigation is free but new sessions get fresh lists.

Fresh and affordable
Alternative
Precompute lists for every user nightly

Cheap at serving time and immune to model outages, but most precomputed lists are never viewed, and none of them reflect today's session. Useful as a fallback tier, not as the main path.

Good fallback

08

Failure modes

08

Failure modes

🔁
The system starts recommending only what it already recommends

The model learns from clicks, but users can only click what was shown. Over months, the same few thousand items absorb most impressions, new sellers get nothing, and engagement slowly falls even though offline metrics look fine.

Reserve a small exploration budget, log the probability with which each item was shown, and correct for position and exposure bias in training. Track catalogue coverage as a top-level metric next to click rate.
⚖️
Training and serving disagree about a feature

The training job computed "purchases in the last 7 days" in UTC, while the serving path used local time. The model ships, offline accuracy is great, and live conversion drops 4% with no obvious cause.

Define features once in the feature store and generate both paths from that definition. Log the exact feature values used at serving time and train on those logs, so training data is what serving saw by construction.
🗄️
The feature store is slow or down

A hot partition in the online store pushes reads past 80 ms. The whole request now misses its budget and pages time out.

Give every dependency a hard deadline. If user features miss theirs, rank with defaults; if ranking misses, return the pre-ranker order; if everything fails, serve the in-memory popular list. Each step down is logged so the drop in quality is measurable.
📦
A bad index or model is pushed

The nightly item tower was trained on a partial day of data, and the new index returns mostly accessories. Every request is now served from broken candidates.

Gate every artefact on automated checks: recall on a fixed query set, category distribution, overlap with yesterday's results. Roll out to 1% first, keep the previous version loaded, and switch back in one config change.
🤖
Bots inflate an item's signals

A seller runs scripted sessions that view their product next to bestsellers. Co-visitation tables pick it up, and it appears in "customers also viewed" across a whole category.

Filter events through bot detection before they reach training tables, cap any single user's contribution to co-visitation counts, and alert on items whose signals jump far faster than their sales.
⏳
The event stream falls behind

A Flink job stalls on a schema change. Session features freeze, and users who just bought a laptop keep being shown laptops.

Alert on consumer lag in seconds, not in records. Read "recently purchased" exclusions from the order service directly as a fallback, since it is the one signal that is embarrassing to get wrong.

09

How to handle the interview

09

How to handle the interview

1

Ask what you are optimising for first. Clicks, purchases and long-term retention lead to different labels and different models. Interviewers notice when the objective comes before the architecture.

2

Draw the funnel with numbers on each stage. Millions to thousands to hundreds to tens, with a latency budget beside each step, explains the whole design in one picture.

3

Name several retrieval sources and the gap each fills. Collaborative signals for known items, content for new ones, trending for new users. One source is a sign you have not run this in production.

4

Explain why two towers can be indexed. The answer is that item vectors do not depend on the user, so they are computed once. That single sentence shows you understand the retrieval and ranking split.

5

Bring up training and serving skew unprompted. It is the most common way real systems fail quietly, and a feature store plus logged features is the standard answer.

6

Cover cold start for both sides. New users get context and popular items until they generate signal; new items get content retrieval and exploration slots.

7

Describe how you would know it works. Offline metrics such as recall and NDCG gate a model; only an A/B test on the business metric decides whether it ships.

8

Have a fallback ladder. Showing what the page looks like when each component fails is a strong signal of operational maturity.

10

How the system grows

11

How the system grows

Stage 1: launch
Popular and rules-based

Bestsellers by category and region, plus "recently viewed". A SQL job computes them hourly. There is no model yet, which is fine, because there is not enough data to train one.

Stage 2: a few million users
Co-visitation and event logging

A nightly job builds item-to-item neighbour lists. Impressions start being logged properly, and the first A/B testing framework appears so changes are measured instead of argued about.

Stage 3: tens of millions of users
Learned ranking

A gradient-boosted tree ranks the union of candidate sources using user and item features. A feature store arrives to stop skew bugs. Lists are computed per request instead of precomputed.

Stage 4: over a hundred million users
Embeddings, ANN and streaming features

A two-tower model with an ANN index becomes the main retriever, a multi-task neural ranker moves onto GPUs, and Flink brings session features into the request path.

Stage 5: mature platform
Sequence models and whole-page optimisation

Transformer encoders over the user's recent actions replace hand-built history features, and the re-ranker starts optimising the full page rather than each slot on its own.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.