InterviewKata
System design · Worked design D44 of 46 · Social and content · Long form

Design a recommendation system

Design the "For you" feed of a large online marketplace: 200 million listings, 30 million shoppers a day, and a home page that should show each of them 50 things they're likely to want, in under 150 milliseconds. The same shape powers "up next" on video sites, playlists in music apps and "people also bought" on product pages: find a few hundred good candidates cheaply, rank them carefully, then make the list varied and fair.

Modules this design uses09 Caching · 13 Vector search · 18 Streams · 23 Batch and stream · 29 AI systems · Designs D40, D41

1. The prompt

"Design the personalised recommendations on a marketplace's home page."

What makes it hard:

2. Frame the problem

Which surface?

The home page "For you" feed: about 50 items, refreshed each visit. "Similar items" on product pages is a follow-up.

Each surface has a different goal and latency budget; pick one and design it well.

What are we optimising?

Purchases per visit, with guardrails: returns, complaints and seller fairness must not get worse.

Clicks alone reward clickbait; the target decides the training labels (9.5).

How big?

200 M active listings, 2 M new ones a day; 100 M monthly shoppers, 30 M a day, ~5 home visits each.

2 M new items a day means cold start is constant, not an edge case.

How fresh?

Within the same visit: what someone looked at a minute ago should count.

Needs a near-line path alongside the nightly batch (9.3).

Constraints?

Never show sold-out or banned items, nothing the person already bought (for one-off goods), at most a few items per seller in one feed.

Business rules live in a re-ranking step, not in the model.

3. What it must do

  1. Return a ranked, varied list of about 50 items for a shopper, quickly.
  2. React within the session to what they view, search and add to their basket.
  3. Learn continuously from what was shown and what happened next.
  4. Handle the new: new shoppers with no history, and new listings nobody has seen.

Not now: search ranking (similar machinery, different input), email recommendations, sponsored placements (that's D41).

4. How well it must do it

TargetWhy this number
Feed served in under 150 ms at p99 speedThe home page waits for it; slower pages sell less.
Session signals used within 30 s freshnessThe next page should reflect the last few clicks.
Always an answer: a fallback list if any part fails surviving failuresAn empty home page is worse than a generic one.
Every new listing shown to some shoppers within a day fairnessSellers leave a marketplace where new items are invisible.
No personal data leaks through recommendations privacy"Because you bought…" on a shared screen can reveal too much.

5. Numbers that change the design

QuantityWorking→ so we…
Feed requests30 M × 5 = 150 M a day ≈ 1,700 a second, ~6,000 at peak→ so serving is a modest fleet; the cost is per-request model work
Scoring everything200 M items × 20 µs per score = 4,000 s of CPU per request→ so retrieve ~1,000 candidates cheaply, then rank only those (1,000 × 20 µs = 20 ms)
Item embeddings200 M × 128 numbers × 4 bytes ≈ 100 GB; compressed to 32 bytes each ≈ 6.4 GB→ so an approximate nearest-neighbour index fits in memory, sharded or compressed
Impression logs150 M feeds × 50 items ≈ 7.5 B impressions a day→ so a stream into the warehouse, sampled negatives for training
New listings2 M a day, with no clicks yet→ so content-based embeddings and an exploration budget (9.4)

Illustrative numbers.

6. The nouns

events           (shopper_id, ts) → item_id, kind, position, surface, request_id      (stream → warehouse)
item_features    item_id → price, category, seller, stock, 7-day sales, embedding_v12     (online store)
shopper_features shopper_id → category affinities, price band, embedding_v12, last_50_views (online store)
co_bought        item_id → [(item_id, score)] top 200                                     (nightly)
ann_index        embedding_v12 → item_id                                                  (memory, sharded)
feed_log         request_id → model version, candidates, scores, final order              (for training and debugging)

7. The contract

GET  /v1/feed?surface=home&limit=50&cursor=…   → [{item_id, reason?}], request_id, next cursor
POST /v1/events   [{request_id, item_id, kind: impression|click|basket|purchase, position, ts}]   (batched by the app)
Internal: POST /rank {shopper_id, candidates[], context} → scores      (gRPC, same data centre)

The shopper comes from the auth token. The request_id ties every later click and purchase back to exactly what was shown and by which model version: without it, training data can't be trusted.

8. First working version

Start simple and still correct: a nightly job computes "bought together" lists and popular items per category; the feed service merges a shopper's recent views with those lists, filters, and returns. Then replace the merge with a retrieve → rank → re-rank pipeline, keeping the simple version as the fallback.

8.1 Serving one feed

8.2 The learning loop

Parked for laterwhy two stages (9.1) · features that match between training and serving (9.2) · freshness within a session (9.3) · new items and feedback loops (9.4) · knowing it works (9.5).

9. Make it hold up

9.1 Retrieve cheaply, rank carefully speed

Each retriever is good at one kind of candidate, so several run in parallel, each returning a couple of hundred: item-to-item ("bought together" with what you viewed), embedding search (items near your taste vector, from a two-tower model), trending in your region and categories, and fresh items for exploration (9.4). Retrievers can be wrong cheaply, because the ranker sees everything they propose. Measure each one by how often it supplies what people end up buying.

9.2 Features that mean the same thing in training and serving correctness

ApproachWhy
✗ BadData scientists compute features in SQL for training; engineers rewrite them in the service for servingTwo implementations drift apart ("last 30 days" includes today in one, not the other) and the model quietly gets worse in production: training-serving skew.
~ GoodLog the features exactly as served, and train on the logsNo skew, but a new feature can only be trained on after it has been logged for weeks.
✓ GreatA feature store: each feature defined once, computed by batch and stream jobs into an offline store (history, for training) and an online store (latest value, for serving); training joins features as of the time of each event, never later; plus logging served features to check for driftOne definition, both paths; no leaking the future into training; skew is measured, not discovered.

9.3 Reacting within the session freshness

Three speeds, each cheap at its own job:

So the boots viewed a minute ago change which candidates come back and how they're scored, without retraining anything.

9.4 New items, new shoppers and feedback loops fairness

ProblemWhat we do
A new listing has no clicks, so no collaborative signalEmbed it from its title, photos, category and price with a content model, so it lands near similar items in the ANN index on day one.
The ranker never shows what it isn't sure aboutReserve a few slots per feed (say 3 of 50) for exploration: items with few impressions, chosen with a bandit rule so promising ones get more traffic. The lab below shows why.
A new shopper has no historyPopular items in their region and the category they arrived from; a few taste questions at sign-up; then the session signals take over within minutes.
Items shown at the top get clicked because they're at the topLog the position of every impression and correct for it in training (position as a feature, set to a fixed value when serving), so the model learns appeal, not placement.
The feed narrows to one interestThe re-ranker enforces variety: at most N per category and per seller in any 10 items.

9.5 Knowing it works quality

9.6 The whole design after the deep dives

10. Interviewer follow-ups

The ranker is down.

Serve the candidates in a simple order (retriever score, popularity), filtered by the re-ranker. Worse than personalised, much better than empty. If retrieval is down too, serve a cached popular list per region, refreshed every few minutes.

Black Friday: 10× traffic.

Pre-scale the ranker fleet (the schedule is known), shrink candidates per retriever from 200 to 100, and cache feeds for a few minutes for logged-out visitors. Shed the exploration slots last, not first, or new sellers lose their biggest day.

"Because you bought…" revealed a gift on a shared tablet.

Sensitive categories (health, gifts, adult) are excluded from explanations and from home-page retrieval unless the shopper opts in; recently bought gifts can be hidden. Explanations come from an allow-list, never raw history.

How do you add a "similar items" carousel on product pages?

Reuse retrieval: item-to-item lists and the item's embedding neighbours, then a lighter ranker with the product as context. Precompute for popular items and cache.

A seller games the system with fake clicks.

Train on purchases and long engagement more than clicks, discount clicks from new or suspicious accounts, and cap how fast an item's features can change in a day. Fraud signals from payments feed back as labels.

Why not one big model end to end?

It can't score 200 M items per request. Two stages let each part do what it's good at: retrieval for recall at low cost, ranking for precision on a small set.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Ranker slow or downA less personal feedLatency and errors per stageTimeout per stage, fallback ordering
Stream job lagsFeed ignores the last few minutesConsumer lagNightly features still there; catches up
Bad model versionFewer purchases, odd itemsCanary metrics by versionAutomatic rollback; previous version kept warm
Feature pipeline bugGradual quality lossDrift checks on feature distributionsTraining refuses data that fails validation
ANN index build failsEmbedding retriever on yesterday's indexBuild job alertServe the last good index; other retrievers unaffected

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: p99 per stage · recall of each retriever · purchases per visit by model version · share of impressions going to new items · feature drift · stream lag.

At 10×: more ranker replicas (the per-request cost); a sharded ANN index; the batch side grows with data, not traffic.

A 60-minute interview

0–5Frame: surface, objective, scale, freshness
5–12Must do, how well, the "score everything" number
12–18Nouns, events with request IDs
18–30First version, then retrieve → rank → re-rank
30–52Deep dives: funnel (9.1), features (9.2), new items (9.4)
52–60Evaluation, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including guardrails and privacy
8–16Must do, how well, numbers
16–24Nouns and contract
24–38First version and the learning loop
38–78Deep dives 9.1–9.5 and the final picture
78–86Follow-ups: outages, peak day, fraud
86–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelPrecomputed lists (bought together, popular), a feed service, event logging, a cache.
SeniorThe funnel with numbers, embeddings and ANN retrieval, a feature store, near-line freshness, cold start, A/B testing with guardrails, fallbacks.
Staff+Feedback loops and position bias, exploration as a budget, point-in-time correctness, model rollout and drift, privacy of explanations, marketplace fairness.

Common mistakes:

13. Try it, check yourself, read more

The exploration lab. Five established items have years of clicks. A new item arrives with none. The feed shows the item with the best estimated click rate, except for a share of impressions spent exploring at random. Choose how good the new item really is and how much to explore; each setting is simulated 20 times over 20,000 impressions.

New item's true click rate
Impressions spent exploring

The established items' true click rates are 4.0%, 3.5%, 3.0%, 2.5% and 2.0%, each with 4,000 past impressions. Exploration here is "epsilon-greedy", the simplest bandit rule; real systems use smarter ones (Thompson sampling, upper confidence bounds).

Scoring one item takes 20 µs and there are 200 M items. How do you serve a feed in 150 ms?

200 M × 20 µs is over an hour of CPU; 1,000 × 20 µs is 20 ms.

A model is great offline and worse in production, and the training features were computed by a different code path from serving. What is this called?

One feature definition for both paths (a feature store) and logging served values prevents it.

Why does a purely greedy feed never discover a great new item?

The data comes from what was shown. A small exploration budget breaks the loop.

What should the A/B test's main metric be for a marketplace feed?

Clicks reward clickbait; purchases with guardrails reward what the business and shoppers want.
ResourceWhy
Deep Neural Networks for YouTube Recommendations (2016)The classic two-stage paper: candidate generation, then ranking.
Wide & Deep Learning for Recommender Systems (2016)A ranking model that combines memorised and generalised signals, from Google Play.
System design for recommendations and search (Eugene Yan)How many companies split offline and online, retrieval and ranking.
Feast documentationAn open-source feature store: offline and online stores, point-in-time joins.