Social & Feed
Design Recommendation Algorithm
01
The problem
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.
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
What it has to do
- 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.
- 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
| Goal | What it costs | How the design balances it |
|---|---|---|
| Relevance | bigger models, more features, more latency | heavy model only on the last few hundred candidates |
| Freshness | streaming infrastructure and frequent index rebuilds | session features in real time, embeddings daily |
| Variety | some loss in predicted click rate | a re-ranking step with explicit diversity rules |
| Exploration | showing items the model is unsure about | a small, measured slot budget for new items |
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
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.
| Quantity | Working | Result |
|---|---|---|
| Recommendation requests per day | 180M x 8 | about 1.44B |
| Average request rate | 1.44B / 86,400 s | about 16,700 / s |
| Peak request rate | 3x average in the evening | about 50,000 / s |
| Items scored by the heavy ranker | 50,000 x 400 at peak | 20M scores / s |
| Ranking accelerators | about 300K scores / s per GPU, plus 50% headroom | roughly 100 GPUs |
| Interaction events | impressions dominate, about 120 per user per day | about 22B a day, 250K / s average |
| Item embeddings | 40M x 128 dims x 4 B | about 20 GB, about 1.3 GB with 32-byte PQ codes |
| User embeddings | 500M monthly users x 128 dims x 2 B | about 128 GB |
| Online feature store | about 2 KB per user, 1 KB per item | about 1 TB plus 40 GB |
| Training data | 22B events x about 300 B with features attached | about 6.6 TB a day |
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
Endpoints and event logging
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
Architecture overview
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 --> RKRequests 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
| Step | Budget at p99 | What happens if it is late |
|---|---|---|
| Load user features and session | 15 ms | rank with default features |
| Candidate generators, in parallel | 25 ms | drop the slow source for this request |
| Merge, filter, pre-rank | 10 ms | rarely late, runs in process |
| Fetch item features for 400 candidates | 15 ms | use cached item features |
| GPU ranking with batching | 35 ms | return the pre-ranker order |
| Re-rank and serialise | 10 ms | rarely late |
| Network and slack | 40 ms | absorbs 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.
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.
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.
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
Deep dive: retrieval and ranking
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"]
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.
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
Design decisions and trade-offs
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 scaleFine 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 scaleEmbeddings 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 mattersOne 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 digestsSeparate 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 stageMore 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 recallWarm-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 freshnessUpdating 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 guardrailsRun 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 affordableCheap 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 fallback08
Failure modes
Failure modes
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.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.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.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.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.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
How to handle the interview
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.
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.
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.
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.
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.
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.
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.
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
How the system grows
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.
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.
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.
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.
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.
