System design interview

Design a recommendation system

Candidate retrieval, ranking, feature stores, cold start and safe rollouts.

Overview
AppRec APIEvent logRetrievalRankerFeaturestoreEmbeddingindexModelregistryTrainingnightlyAppRec APIEvent logRetrievalRankerscores each oneFeature storeEmbedding indexnearest itemsModel registryTrainingnightly

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  • Cache and queue
  1. A user opens the app, and it asks for the home page with 20 items.
  2. The API reads the user's features from the feature store: a 128-number taste vector, the last 50 views and their language.
  3. It sends the user vector to retrieval, which asks every candidate source for its best items.
  4. The index returns the 400 nearest items. Retrieval adds 300 often viewed with the user's recent items, 150 trending, 150 new and 100 from followed creators.
  5. Retrieval merges the 1,100 results, removes duplicates and seen items, and is left with about 1,000 candidates.
  6. It returns them to the API, each tagged with the source it came from.
  • App
  • Rec API: assembles the page
  • Event log: shown and clicked
  • Retrieval: about 1,000 candidates
  • Ranker: scores each one
  • Feature store: online and offline
  • Embedding index: nearest items
  • Model registry: models, embeddings
  • Training: nightly
On this page

In 2 minutes

I would build the page as a funnel. The API reads the user's features, a taste embedding and recent views, from an online feature store. Retrieval then gathers about 1,000 candidates from several sources: the 400 nearest items from an approximate embedding index, items often viewed together, trending, fresh, and followed creators. Scoring all 10 million items would take 500 seconds, so a ranker scores only the 1,000, in about 50 ms, with a model predicting click and satisfied watch.

Final rules drop seen and blocked items, cap items per creator, add variety and put one exploration item in the page. Item features are held in each ranker's memory, 10 GB, because reading them remotely would be 4.6 million reads a second. Every impression is logged with the exact features used, about 600 GB a day, so training sees what serving saw and there is no skew. Clicks are weighted by position, and 5% of slots go to new items, about 2,000 impressions each.

A new model passes offline tests, a shadow run and a staged A/B ramp, with automatic rollback. If a stage is late, the page falls back to simpler lists, so it is never empty.

Hide the answers and test yourself

Step 1 of 6: Clarify the scope

Interviewer asks

Design the home page of a video or shopping app. For each user, pick the 20 items to show from a catalogue of 10 million, in about 200 milliseconds. The page should fit the user's taste, stay fresh, and give new items a fair chance.

Questions to ask first

Ask these before you draw anything. Under each one is why its answer changes the design.

How big is the catalogue, and how fast do new items arrive?

Millions of items rule out scoring them all. Fast arrivals mean new items need a way to be shown before they have any history.

What are we optimising: clicks, watch time, purchases, or long-term satisfaction?

The target decides what the model predicts. Clicks alone reward clickbait, so we usually blend several outcomes.

What is the latency budget for the page, and how many requests a second do we serve?

A budget of 200 ms forces a fast first stage and a bounded number of candidates for the heavy model.

Do we know most users well, or are many of them new or logged out?

Cold start needs its own path, because there is no history for a model to learn from.

Which rules must always hold: hide what was seen, blocked creators, limits per creator?

Rules apply after scoring, and some are safety or legal requirements rather than preferences.

Requirements

Functional

  • Return 20 ranked items for a user's home page, built from several candidate sources.
  • Filter out seen, blocked and unavailable items, and limit how many come from one creator.
  • Show new users and new items something sensible, and learn from their first actions.
  • Log every impression and action, with the features used, so models can be retrained.

Non-functional

  • Page assembled in under 200 ms at p99, at 4,600 requests a second at the peak.
  • Rank 1,000 candidates in under 60 ms.
  • A new item gets about 2,000 impressions within a day of arriving.
  • A new model reaches all users only after an A/B test, and a bad one is rolled back in minutes.
  • A feature has one definition in training and in serving.
  • If a stage fails, the page degrades to a simpler or cached list. It is never empty.

Step 2 of 6: Size it

Scoring everything would take 500 seconds a request, so the first stage must cut 10 million items to about 1,000 in 40 ms. Retrieval quality then caps what the ranker can do, and the log feeding training is the biggest storage cost.

Assumed for the exercise, not facts about a real system:

  • 10 million items and 50 million users. 100 million home page loads a day, each showing 20 items. The peak is 4 times the average.
  • An item embedding has 128 numbers of 4 bytes. The ranker scores one candidate in about 50 microseconds of one core.
  • 50,000 new items arrive each day. An impression log row is about 300 bytes with its features.
  • Item features take about 1 KB each. 5% of the slots on every page are kept for exploration.
Requests per second
100M ÷ 86,400 s ≈ 1,157/s; × 4
≈ 1,160/s average, ≈ 4,630/s at the peak
Latency budget
features 10 + retrieval 40 + ranking 60 + diversity rules 10 + network 30 ms
150 ms, leaving 50 ms of the 200 ms spare
Scoring cost
10M items × 50 µs = 500 s. 1,000 candidates × 50 µs
500 s for everything, 50 ms for 1,000
Ranker CPU at the peak
4,630/s × 50 ms of one core = 231 cores; run at 50% load
≈ 460 cores
Embedding index per replica
10M × 128 × 4 bytes = 5.1 GB; the graph links roughly double it
≈ 10 GB, held in memory
Impression log
100M × 20 = 2B impressions × 300 bytes
≈ 600 GB a day

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

The App asks the Rec API for a page. The API reads the user's features from the Feature store: an embedding of their taste, their recent views, their language. It sends them to Retrieval. Retrieval gathers about 1,000 candidates from several sources. The main one is the Embedding index, which finds the items closest to the user's vector. Other sources add items often viewed together, trending items and fresh ones.

The API sends the candidates to the Ranker, which uses the current model from the Model registry and the item features to predict how likely the user is to enjoy each one. It sorts them. The API then applies rules: drop seen and blocked items, mix topics, limit items per creator, and put exploration items in a few slots. The top 20 return to the app.

The API also writes what it showed to an Event log, with the features it used, and the app sends clicks and watch time to the same log. The log updates fast-moving features in the feature store within a minute. A nightly Training job reads the history, builds a new model and new embeddings, and publishes them to the registry, which feeds the ranker and the index.

Other paths through the system

Rank and show the page

  1. The API sends the 1,000 candidates and the user's features to the ranker.
  2. The ranker reads each candidate's features from its in-memory copy of the item table, refreshed every minute from the feature store.
  3. It combines the user, item and context features into one row per candidate, with the same code that training used.
  4. It scores all 1,000 in about 50 ms and returns them sorted, with each score.
  5. The API removes blocked items, caps each creator at 2, mixes topics, fills 1 slot with an exploration item and returns 20 items.

Learn from feedback

  1. The user watches the third item for 90 seconds and taps hide on the fifth.
  2. The API appends the impressions, the actions, the positions and the exact features used to the event log.
  3. A stream job updates fast-moving features, such as views in the last hour, within a minute.
  4. The nightly job reads the history, joined with the logged features, and trains a new model.
  5. It saves the model and new item embeddings to the registry, marked as a candidate for an A/B test.
  6. The index rebuilds from the new embeddings.
  7. After the test passes, the ranker loads the new model. The old one stays ready for a rollback.

Step 4 of 6: Go deep on the hard part

Retrieve, rank, then apply rules

Interviewer asks

How do you get from 10 million items to 20 in 200 ms, and why not just score every item?

Scoring one item takes about 50 microseconds, so all 10 million would take 500 seconds.

Read more: Retrieve, rank, then apply rules

The design is a funnel with three stages, each slower per item than the one before and working on fewer items.

  • Candidate generation. Several cheap sources each return their best items, and the results are merged. The main source is nearest neighbours over embeddings. A model learns a vector for each user and each item, so that items a user would like lie close to the user's vector. The index finds the closest 400 items by approximate search, in about 15 ms, giving up a few percent of recall for a large gain in speed. Other sources are items often viewed together, computed offline from watch histories into a table; trending items for the region; fresh items; and recent items from followed creators. Several sources mean that one weak spot does not empty the page.
  • Ranking. A heavier model scores each of the roughly 1,000 candidates with many features: the user, the item, how they fit, the time of day and the device. It predicts several outcomes, such as the chance of a click and of a satisfied watch, and a weighted blend gives the score. 1,000 × 50 µs is 50 ms.
  • Rules. Final code, not a model, picks the 20. It drops seen, blocked and unavailable items and caps items per creator. It also picks for variety. After choosing each item, it lowers the score of candidates that are very similar to those already chosen, so the page does not show ten near-copies.

Retrieval sets the ceiling. An item that retrieval does not return can never be shown, however good the ranker is. So measure the recall of retrieval: of the items a user later enjoyed, how many were among the candidates. Improve retrieval and ranking separately.

Budget. Features 10 ms, retrieval 40, ranking 60, rules 10, network 30 add up to 150 ms. That leaves 50 ms for slow requests. Give each stage a deadline. If the ranker is late, return candidates in retrieval order instead of an empty page.

Features online and offline

Interviewer asks

How do you give the model fresh features at serving time without training on different numbers from those it sees live?

A model is only as good as the features it sees, and they live in two places.

Read more: Features online and offline
  • Online store. An in-memory key-value store with reads under 5 ms. It holds the user's embedding and recent views, and per-item counters such as views in the last hour.
  • Offline store. A columnar history, partitioned by day, with every feature's value over time. Training reads from it.

Training-serving skew is the classic failure. If training computes views in the last hour with a SQL query, and serving reads a streaming counter, the two differ in subtle ways: time zones, duplicate events, late events. The model learns from one definition and is judged on another, and it does worse live than in testing. Two defences work together.

  • One definition. Write each feature once, in code that both the batch job and the live service run.
  • Log what was served. The API writes the exact feature values the ranker used for each impression to the event log. Training reads those logged values for the fast-moving features, and never recomputes them. What the model learns from is what it saw. The cost is the log, about 600 GB a day. For features that were not logged, join them as of the time of the impression, never as of the training run, or the model peeks at the future.

Where item features live. A page needs the features of 1,000 items. Read from a remote store, that is 4,630 requests × 1,000 = 4.6 million key reads a second at the peak. Instead, each ranker holds the item side in its own memory: 10 million items × 1 KB is 10 GB, refreshed from the feature store every minute. Only the user's features are fetched per request, in one read.

Freshness. Counters update within a minute through the stream. The user's recent views update in seconds. Embeddings are rebuilt nightly. Watch for drift: compare each feature's live distribution with its training distribution every day, and alert on a sudden rise in missing values.

Feedback, cold start and safe rollout

Interviewer asks

How do you learn from clicks without being fooled by them, treat new items and users fairly, and ship a new model safely?

Signals. Users never say what they like, so we infer it.

Read more: Feedback, cold start and safe rollout

A click is noisy, because a catchy thumbnail gets clicks that end in a quick exit. Treat a watch of more than half the item, or a purchase, as a stronger positive. Treat hide and not-interested as strong negatives. Predict several of these and blend them, so clickbait cannot win on clicks alone.

Position bias. Items in slot 1 get clicked more because of where they sit. If slot 1 is seen 90% of the time and slot 10 only 30%, a click on slot 10 is better evidence. Weight each training click by 1 ÷ the chance the slot was seen: 1.1 for slot 1, 3.3 for slot 10. Cap weights at 5 to keep training stable. Another way is to feed the position to the model as a feature during training and fix it to a neutral value when serving.

Selection bias and exploration. The model learns only from what we showed. An item never shown never gets data. So fill 5% of the slots, 1 in a page of 20, with exploration items, mostly new ones. With 5% of 2 billion impressions, that is 100 million a day, across 50,000 new items, which is 2,000 each. That is enough: at a true click rate of 5%, 2,000 impressions measure it to within about 1 point, since the standard error is √(0.05 × 0.95 ÷ 2,000) ≈ 0.5%.

Cold start. A new user has no history. Show popular items for their region and language, plus categories they chose at sign-up. After a few taps, average the embeddings of the items they touched into a rough vector. A new item has no history either, so give it an embedding from its title, tags and image, and rely on exploration.

Rollout. Retrain the model daily. Then pass four gates: offline metrics on held-out days, a shadow run that scores live traffic without showing it, a 1% A/B test for 2 days, and a ramp to 5%, 50% and 100%. Assign users to groups by hashing the user ID with the experiment name, so assignment is stable. Watch guardrails, such as watch time, hides, latency and errors, and roll back automatically on a drop. Stamp every impression with the model version, and keep a 1% holdback on an old model to measure long-run effects.

Step 5 of 6: Name the trade-offs

Architecture

Retrieve about 1,000 candidates, then rank them over One big model scoring the whole catalogue

Ranking costs 50 ms, not 500 seconds. The cost is that an item retrieval misses can never be shown, so retrieval recall caps quality.

Item features at ranking time

A copy of the item table in each ranker's memory over A remote read for every candidate

It avoids 4.6 million reads a second at the peak. The cost is 10 GB of memory per node and features up to a minute old.

Training data

Features logged at serving time over Recomputing features offline

The model trains on exactly what it saw, which removes skew. The cost is about 600 GB of log a day.

New items

Reserve 5% of slots for exploration over Show only the highest predicted scores

Each new item gets about 2,000 impressions to learn from. The cost is a small drop in short-term engagement.

Step 6 of 6: Say what breaks next

Questions an interviewer asks once the design is on the board. Answer each one out loud first.

How do you serve a brand new user?

Start with popular items for their region and language, plus the categories they chose when signing up. After a few taps, build a rough taste vector from the items they touched. Move them to the full model once they have around 20 interactions.

How do you stop the same items from appearing again and again?

Keep a per-user list of recently shown items for a few days, and filter against it at retrieval. Add a feature for how many times an item was shown without a click, so ignored items sink. Apply the same filter when the user scrolls to the next page.

How would you serve this from several regions?

Copy the index, the model and the item features to each region, since they are read-only and rebuilt daily. Write user features and the event log near the user, and replicate them in the background. A region that loses its link falls back to its last copy of the data.

What happens if the ranker is slow or down?

Degrade in steps. If ranking misses its deadline, return candidates in retrieval order. If retrieval fails too, serve a cached popular list for the user's region, refreshed every few minutes. Log every fallback, and alert when the rate rises.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.