RecSys · textbook
Trainer Widgets Revision About All chapters ← Runtime All chapters →

Part VI · Design · chapter 19 of 19

System design

The last chapter collects the previous eighteen into a single procedure. In an interview «design a recommender system for X» is not a question about knowing architectures but about the order of thinking: where you start, what you fix before naming a single model, and at which moment you admit that the first solution will have to be thrown away.

What to take away
  • First a simple working solution, then criticism, then the full one. A complex system designed from scratch does not work and cannot be patched into working.
  • Goal setting cannot be skipped. A crude mistake here will not be rescued by any model below it — and skipping it is always tempting.
  • The size of the catalogue determines the architecture. Up to 75 000 candidates no funnel is needed at all, and refusing one is the right decision, not a simplification.
  • A rare target event is measured through a proxy not out of stubbornness. Catching the same relative effect at a conversion of 0.5% instead of 40% costs 133 times more traffic.

1. Gall's law

The formulation to start an answer with

«A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work».

Hence the general shape of an answer: first build a simple but working solution → criticise it → build a fuller one.

And the subtlety that keeps this from being an empty slogan: the criticism leans on those features of the problem that we came to understand while building the simple solution. That is, the simple solution is needed not only as a fallback — it is needed as an instrument of understanding.

About temporary solutions

They are part of the life of any engineer and any runtime. They are best accepted with an understanding of what the long-term correct solution is — so as not to do something that fundamentally contradicts it.

An honest caveat: sometimes merely working out the correct solution is extremely expensive. Then it is better to trust experience — some working solution is in any case better than none.

2. Five steps

1. Goal setting business metric → offline → target 2. Constraints catalogue, SLA, intent, filters, biases 3. Tech architecture shards, filtering, data delivery 4. ML architecture retrieval, model, features, loss 5. Experiment split, metrics, guardrails criticise → build a fuller solution the order is approximate: if goal setting does not come at once, you can start with the second point — but it has to be fixed in any case before moving on to the technical steps
The scheme is unrolled twice: the first pass gives the simple solution, the second comes after the criticism.

Step 1: goal setting

  1. Find the key business metric we want to grow.
  2. Tie it to an offline metric a model can grow.
  3. Determine the key target of the model.
Why this stage cannot be skipped

A crude mistake here will not be rescued by any complex model below. If you are optimising the wrong thing, the quality of the implementation does not matter.

And skipping it is always tempting: many people are impatient to start doing something already, and often that is a useful trait — which is exactly why goal setting looks like the most logical candidate for a quick skip.

Partly to compensate for that temptation we build the simple solution first: to reach something working quickly and come back to the goal with an understanding of the problem.

Step 2: constraints

A purely brainstorming stage — it prepares the material basis for every decision that follows. What to collect:

The first number worth computing out loud

The size of the catalogue determines the architecture more than anything else. Let the ranking model spend 4 µs per candidate and the response budget be 300 ms:

CatalogueFull scanDoes it fit?Selection needed by
3e+0312.0 msyes
5e+052000.0 msNO7 times
5e+082000000.0 msNO6667 times

The threshold where a full scan stops fitting: 75 000 candidates.

The numbers are reproduced by the script _tools/design_demo.py in this repository.

Below the threshold candidate generation is not needed at all — and that is not a simplification but the right decision: any funnel loses candidates, and the ceiling of quality is set by the first stage. A candidate who begins an answer with «first we build a two-tower retrieval» without asking about the size of the catalogue shows that they are applying a template rather than thinking.

Steps 3–4: technology and ML

A trick for generating ML ideas fast

Formulate the key actors — most often the user, the item and the context. Then invent ideas for candidate generation and for features:

  • for each actor separately — three cells;
  • for each pair of actors — three more.

The result is a 3 + 3 grid that almost always covers everything essential. And it is convenient to speak through out loud as the structure of the answer — the interviewer sees a system rather than an enumeration.

Actor or pairExample features
Userpurchase frequency (with a time limit, so the features «travel» less), average basket, embeddings of favourite categories
Itemsales of the good, of its category and of its manufacturer over \(X\) days; impressions, CTR, CVR and their weighted versions; the embedding
Contextday of the week, seconds since the start of the day, the embedding of the surface, the embedding of the basket
User × itemthe dot product of the towers; how many times the user took this good, this category, this manufacturer over \(X\) days
User × contexthow much the user usually buys on this day and hour; how typical the current basket is for them
Item × contexthow often the good is taken at this time; its CTR and CVR at this time; the closeness of the good to the basket

Step 5: the experiment

The check before launching

Fix the concrete hypothesis you are testing. Otherwise, with a large number of metrics, you will always find something pleasant that turned significant — and the experiment turns into a search for confirmations instead of a test.

And before launching it is worth estimating whether there is enough traffic: the sample size and the MDE are computed in advance, not after the experiment failed to turn significant.

3. Case 1: e-grocery

The anchor example, against which the differences are easier to show afterwards.

Constraints

Product ones. Recommendations on very many surfaces, which actively cannibalise each other. The user is well known: a large share of regular customers, nothing works without a login — cold start is less relevant. And the user already has an intent: almost nobody comes in «just to scroll». The task is not to get in the way too much while selling something else along the road.

Technical ones. The catalogue at a location is most often up to 3000 items and well structured. There are tens of millions of users and hundreds of requests per second. The response budget, counting prefetches, is up to 300 ms. Goods become available and unavailable instantly, and they spoil, so new arrivals have to appear in the output quickly.

Goal setting and its criticism

The simple first approach. The key business task is the growth of turnover. Offline we proxy it with NDCG where the gain is the price of the good. Hence the obvious target — a purchase — and the score is assembled as

$$ \text{score} = p(\text{buy}\mid \text{show}) \cdot \text{price} $$

or the price is sewn straight into the ranking loss. The dataset follows explicitly: a purchase against an impression in the slate.

And now the criticism — that same second pass through the scheme:

  • money now may not correspond to money in the long run; the business would rather grow the sum over users on a long horizon;
  • the metric does not account for users buying several identical goods;
  • and it does not account for the cannibalisation of surfaces at all.

Note: not one of the three problems is fixed by improving the model. All three are about the formulation.

Why cannibalisation is not a detail

Two surfaces, each giving +10 units of turnover in isolation:

Overlap of audience and assortment0.00.20.50.81.0
total gain+20.0+18.0+15.0+12.0+10.0
loss against the sum0%10%25%40%50%

The numbers are reproduced by the script _tools/design_demo.py.

At full overlap the second surface adds nothing: the user would have bought the same thing without it. An A/B of each surface separately will show a sum of 20, while a joint launch gives less — and the difference grows the closer the surfaces are.

This is a classic reporting trap: the teams honestly show their gains and the total turnover does not grow. It is cured by metrics at the level of the user rather than the surface, and by an uplift formulation — «a purchase on this surface relative to a purchase at all».

DecisionWhy so
No shardingwith such a catalogue and such load there is no point in it
A static profile store, refreshed once a dayusers rarely buy more often than once a day
A static item index, built once a daythe arrival of a new good is known in advance
A cache of available goods right on the service + a follow-up filterfiltering is critical: availability changes instantly
A filter by the current basket and by legal restrictionsdo not show what has been added; tobacco, alcohol
Where this scheme will start to break

It is useful to name that yourself, without waiting for the question.

  • A user may assemble a basket over a long time. Then actions inside the session become important — especially for those we know less about. Which means a runtime loop for the user part is needed.
  • The solutions against cannibalisation will require a full shared feature layer.
  • Growth of the catalogue will require splitting candidate generation and features into separate services.

Re-ranking: the hygienic minimum is softmax sampling against the feedback loop, plus categorical diversity and separate work with new arrivals through bandit blending.

The experiment: the split is by user; the key metric is integral turnover; the auxiliary ones are margin and the conversion of the surface; the guardrails are turnover by category (did we skew the output) and conversion by position (did we break the top).

4. Case 2: a streaming service

From here on, only what differs significantly from the anchor case.

The main problem of the case: the target event is too rare

The business needs subscriptions. But the number of subscriptions is a bad metric for an A/B: the event is very rare and will not turn significant in a short experiment. Exactly how bad:

Base conversionNeeded per groupMore expensive than at 40%
0.4009.42e+031.0
0.2002.51e+042.7
0.0501.19e+0512.7
0.0106.22e+0566.0
0.0051.25e+06132.7

The numbers are reproduced by the script _tools/design_demo.py; a relative gain of 5%, significance 0.05, power 80%.

The number of observations is proportional to \((1-p)/p\) at a fixed relative effect. So catching the same effect at a conversion of 0.5% instead of 40% costs 133 times more traffic.

A proxy metric is taken not out of an analyst's stubbornness — a direct measurement simply does not fit into any reasonable experiment.

How such a proxy is built — the whole recipe
  1. Pick a list of base metrics with a plausible long-term effect: watching series (they have better retention), discovery and the diversity of what is consumed, frequency of use.
  2. Build a dozen candidate combinations out of them.
  3. Work out retrospectively which of them show the long-term effect better — by matching similar users or by adjusted correlations. A model can be trained too, but interpretability is needed.
  4. Validate on a golden set of past experiments — those A/Bs whose long-term outcome is already known.

The fourth point is the one usually forgotten, and it is the real validation: without it the proxy remains a hypothesis.

Onboarding: a trick worth being able to explain

There are many cold users, so onboarding is needed. The naive scheme: show the top of the popular, the user picks, recompute the top. The problem is that the top barely changes.

The trick: after a choice, remove from the sample all the users who also liked what was chosen, and rebuild the top. Let us simulate on an audience of five taste groups of different sizes, five questions:

SchemeWhich groups were asked aboutGroups covered
naive, the pool does not change0, 0, 0, 0, 01 of 5
remove those who liked it0, 1, 2, 3, 45 of 5

The numbers are reproduced by the script _tools/design_demo.py.

The naive scheme asks five times in a row about one and the same taste: the largest group does not go anywhere, and every next question brings no new information. Removal turns the top into «the top for those who did not like the previous one» — and in five questions we go round all the groups.

The formulation worth carrying away: we choose the next question so that it splits the remaining uncertainty as much as possible, not so that it is the most popular.

A carousel interface adds a story of its own: the output consists of blocks — «continue watching», «similar to…», genre selections. One can choose the blocks and rank inside them, or generate a common pool and slice it; in practice both are done. The outer ranking between blocks is often bandit-based because of heterogeneity: comparing «continue watching» with a genre selection using one model is hard — they have a different nature and a different scale of response.

5. Case 3: a media feed

Scale changes everything.

A second entity appears: the author

This is the main difference of the case, and it changes the formulation. The author has their own interests and expectations: authors count on impressions, and their absence produces complaints. The system stops being two-sided «user — item» and becomes three-sided.

The typical approaches: item exploration built into the model, PID controllers to stabilise the volume of impressions, explicit quotas.

And the consequence people forget: authors are paid for impressions, so fraud begins — bots are run, CTR is inflated. In a good system the target is antifraud-aware, as is the objective metric, and the teams work closely together.

The interface influences the target

A subtlety worth naming in an interview. On large interfaces — a feed, selections — long sessions can be optimised. On small ones — one clip on the screen, a carousel — rather not: long articles and videos will be skipped, and we will get artefacts in the metric.

Multimodality adds more: articles, videos and short clips are mixed into one feed, so both the representations and the metrics have to be comparable across content types. «Watched to the end» for an article and for a clip are different events with different prices.

6. Three systems side by side

E-groceryStreamingMedia feed
Catalogue~3 000 at a location300–500 thousandhundreds of millions
Candidate generationcan go without itneeded, no shardsneeded + sharding
Intentstrong, a purchasefrom a single itemabsent
Cold startless relevantcritical for userscritical for items
Business goalturnover → NDCG with gain = pricesubscriptions → a proxy from watch timeattention + the happiness of authors
The key paincannibalisation of surfacesa rare target eventscale, moderation, fraud

Note that not a single row is about the model. The ranking architecture in all three cases will be similar; what distinguishes the systems is everything else — and that is exactly what is asked about.

And one shared constraint: the budget

How to lay 300 ms out over the stages — it is useful to keep the orders of magnitude in mind:

StagemsShare
network and parsing the request206.7%
the user profile3010.0%
candidate generation4013.3%
filtering155.0%
features6020.0%
ranking8026.7%
re-ranking and blending258.3%
serialisation and the network back206.7%
in total29096.7%

The numbers are reproduced by the script _tools/design_demo.py.

The margin is 10 ms, and all of it will go on the tail of the distribution: the budget has to be held at p99, not at the mean. And note where the money is: features and ranking eat 47%, so speed-ups are looked for there first, rather than in candidate generation, where people instinctively look first.

7. The interview checklist

What to ask before answering
  1. Which business metric? And immediately — how often the target event happens. If it is rare, the conversation about a proxy starts here, not at the end.
  2. The size of the catalogue and the number of users? That decides whether a funnel is needed at all.
  3. Does the user have an intent? A feed and a search are different problems, though formally one.
  4. The response budget? 50 ms and 300 ms are different architectures.
  5. Who is known: users, items, both? Hence which cold start is critical.
  6. Is there a third side — authors, sellers, advertisers? That changes the formulation entirely.
  7. Which filters are mandatory? Legal restrictions are designed in from the very beginning, not bolted on.
Five mistakes that are visible at once
  1. Starting with the architecture. «We will take a two-tower retrieval and a DCN on top» — before the size of the catalogue has been established. It shows a template instead of thinking.
  2. Skipping goal setting. The most expensive mistake: nothing below in the stack will rescue it.
  3. Not naming where the solution will break. A good answer contains its own criticism; waiting for it from the interviewer is a missed opportunity.
  4. Forgetting the experiment. A system that cannot be measured has not been designed.
  5. Not naming the guardrail metrics. A sign that the person has not launched anything into production: there, something always breaks somewhere nearby.

Interview questions

Design a recommender system for X. Where do you start?

With five steps in this order: goal setting (business metric → offline metric → the model's target), constraints (catalogue, users, SLA, filtering, intent, biases), the technical architecture, the ML architecture, the experiment setup. Then criticism of what came out and a second pass.

The justification is Gall's law: a complex system that works has always evolved from a simple one that worked, while one designed from scratch does not work and cannot be patched. And the simple solution is needed not as a fallback but as an instrument of understanding: we will criticise on the basis of what we came to understand while building it.

The first things I will ask before any architecture: which business metric, how rare the target event is, the size of the catalogue and the response budget.

Why can goal setting not be skipped?

Because a crude mistake at this stage will not be rescued by any model below: if the wrong thing is being optimised, the quality of the implementation does not matter.

And it is exactly the stage one wants to skip: many people are impatient to start doing things, and usually that is a useful trait — which is why it is dangerous precisely here. A partial compensation is to build the simple solution first, to reach something working quickly and come back to the goal already understanding the problem.

How do you tell whether candidate generation is needed?

Compute it. If the ranking model spends around 4 µs per candidate and the budget is 300 ms, a full scan fits up to roughly 75 thousand candidates. A catalogue of 3000 items is scanned in 12 ms — no funnel is needed at all. A catalogue of 500 thousand requires a selection by 7 times, hundreds of millions by thousands of times.

And it matters that refusing the funnel on a small catalogue is not a simplification but the right decision: any funnel loses candidates, and the ceiling of quality is set by its first stage.

The business needs subscriptions. Why can subscriptions not be measured?

Because the event is too rare. The number of observations for a fixed relative effect is proportional to (1−p)/p, so catching the same gain at a conversion of 0.5% instead of 40% costs 133 times more traffic: 1.25 million per group against 9.4 thousand.

A proxy metric is needed — sensitive in a short A/B and correlated with the long-term goal. It is built like this: choose base metrics with a plausible long-term effect (watching series, discovery, frequency of use), assemble candidate combinations out of them, select the best retrospectively and always validate on a golden set of past experiments with a known outcome. The last step is the real validation.

How do you generate feature ideas quickly?

With a 3 + 3 grid. Write out the actors — usually the user, the item and the context — and invent features for each one separately and for each pair.

For example: the user — purchase frequency and average basket; the item — sales and CTR over X days; the context — day of the week and the embedding of the surface; user × item — the dot product of the towers and how many times they took this category; user × context — how much they usually buy at this hour; item × context — the good's CTR at this time.

The grid almost always covers what is essential, and it is convenient to speak through out loud as the structure of the answer.

What is surface cannibalisation and what do you do about it?

The situation where two surfaces show one user an overlapping assortment and their gains do not add up. If each in isolation gives +10, then at half overlap together they give +15 rather than +20; at full overlap +10, that is, the second adds nothing.

A classic reporting trap: the teams honestly show the gains of their surfaces while the total turnover does not grow. It is cured by metrics at the level of the user rather than the surface, and by an uplift formulation — a purchase on this surface relative to a purchase at all. And uplifts require a full shared feature layer.

How do you onboard a cold user?

Show the top of the popular, let them choose — and then the key trick: remove from the sample all the users who also liked what was chosen, and rebuild the top.

Why: without the removal the top barely changes and the next question brings no new information. In a simulation on five taste groups the naive scheme asks five times in a row about one and the same group — 1 of 5 covered; with removal all 5 are covered.

The general formulation: we choose the next question so that it splits the remaining uncertainty as much as possible, not so that it is the most popular.

What changes when authors appear in the system?

The system stops being two-sided and becomes three-sided. Authors have their own expectations: they count on impressions, and their absence produces complaints. The instruments are item exploration inside the model, PID controllers to stabilise the volume of impressions, explicit quotas.

And the consequence people forget: authors are paid for impressions, so fraud appears — bots and inflated CTR. The target and the objective metric have to be antifraud-aware, otherwise the system will conscientiously optimise the inflation.

How do you lay the response budget out over the stages?

The orders of magnitude at a budget of 300 ms: network and parsing 20, the profile 30, candidate generation 40, filtering 15, features 60, ranking 80, re-ranking 25, the answer 20 — 290 in total, a margin of 10 ms.

Two conclusions. First: the margin will go on the tail of the distribution, so the budget is held at p99 rather than at the mean. Second: features and ranking eat 47% of the budget, and speed-ups should be looked for there first, not in candidate generation, where people look instinctively.

One-screen cheat sheet

Gall's law

Simple and working → criticism → full. The simple solution is an instrument of understanding.

Five steps

Goal → constraints → technology → ML → experiment. And a second pass.

The first number

The scan threshold ≈ 75 000 candidates. Below it a funnel is neither needed nor useful.

A rare event

n ∝ (1−p)/p. 0.5% against 40% — 133 times more traffic. Hence a proxy.

The 3+3 grid

User, item, context — separately and pairwise. It covers what is essential.

Cannibalisation

At full overlap the second surface gives zero. Measure per user.

Onboarding

Remove those who liked it: 5 groups of 5 instead of 1 of 5.

The budget

Features and ranking — 47%. Hold it at p99, not at the mean.

Primary sources

That was the last chapter

Nineteen chapters followed the path of a request: from the statement of the problem and its measurement — through candidate generation, ranking, work with sequences and with the slate — to engineering and back to design.

If one thought is worth taking away from all of it, let it be this: in recommendations almost every technical decision is derived from a constraint rather than from fashion. The funnel comes from the latency budget. Factorisation comes from sparsity. Two towers come from the number of candidates. The stochasticity of the output comes from the need to measure. A good interview answer differs from a bad one not in the set of names but in whether the person names the constraint before the solution.

Next come the revision questions, the widgets and the walkthrough of the open-sourced code of a real feed, where all of this is visible in working form.