System design interview
Design a trending topics service
Count-min sketches, top-K merge, baselines and spam-resistant lists.
Press Play to follow a request through the design, or step through it.
- Client and edge
- Service
- Data store
- Cache and queue
- A worker reads events from its partitions. Each one is a topic, a region, a user and a time.
- It checks the topic against the blocklist, which it keeps in memory and refreshes every minute. A banned topic is dropped.
- For a user it has not seen on this topic in 15 minutes, it asks for the account's age and trust.
- The account is 3 days old, so the mention counts for 3 tenths of a mention and is marked as from a new account.
- It adds the mention to the sketch under its region and worldwide keys, and to the heaps. Every 10 seconds it sends its 13 top-200 lists to the scorer.
- Topic stream: keyed by topic
- Account signals: age, trust
- Count workers: dedupe, sketch, top K
- Blocklist: banned terms
- Baseline: 7 days of sketches
- Trend scorer: merge, then score
- Trends cache: list per region
- Trends API
- App
On this page
In 2 minutes
I would partition the post stream by a hash of the topic, so every event for one topic reaches one of 128 count workers. Each worker drops banned topics, counts each user once per topic per 15 minutes, and weights new accounts lower. It adds each event to a count-min sketch per minute under two keys, its region with the topic and worldwide with the topic. The sketch is 5 rows by 65,536 counters, about 79 MB for 60 minutes, whatever the number of distinct topics.
It can only over-count, by at most about 20 mentions in a 15-minute window at the peak. Thirteen min-heaps of 200 keys per worker, one per region and one worldwide, supply the candidates. A scorer joins the 128 lists once a minute, with no double counting, because each topic lives on one worker. It scores each candidate with at least 300 mentions as a smoothed ratio, current plus 50 over expected plus 50.
Expected is the same hour on the last 7 days, read from stored hourly sketches, about 28 GB. This way a marathon at 4,000 against 200 scores 16, a big sport topic scores 1.09, and a junk topic with 5 posts scores 1.1. The top 50 for each of 12 regions and worldwide go into a cache that the API reads at 14,000 requests a second. New fast-rising topics are held for review before they show.
Step 1 of 6: Clarify the scope
Interviewer asks
Design the trending tab of a social app. Users post hundreds of millions of times a day, many with hashtags. Show the topics that are rising fastest right now, for each region and worldwide, refreshed about every minute.
Questions to ask first
Ask these before you draw anything. Under each one is why its answer changes the design.
- Does trending mean the most posts, or the fastest growth?
Most posts is only counting, and the same big topics win every day. Fastest growth needs a baseline to compare the present against.
- Over what window do we measure, and how often must the list refresh?
The window sets how many time buckets we keep. A refresh every minute allows batching and caching that a one-second refresh would not.
- Is the list per region, per language, or worldwide, and how many lists are there?
Each list is more keys to count and to cache. Thirteen lists are cheap, but thousands of city lists change the design.
- Do counts have to be exact, or is a small over-count fine?
Accepting a small error lets us use fixed-size sketches, which stay small however many distinct topics arrive.
- How do we treat spam, banned terms and coordinated campaigns?
Everyone sees this list, so a gamed or abusive entry is a product failure. It needs filtering built in, not added later.
Requirements
Functional
- Show the top 50 trending topics for each of 12 regions and worldwide, refreshed about every minute.
- Rank by how unusual the current activity is for the topic, not by raw volume.
- Count each person once per topic in a window.
- Keep banned, abusive and obviously gamed topics off the lists.
Non-functional
- Ingest 35,000 topic events a second at the peak.
- A rising topic shows up within 2 minutes.
- Counting memory stays fixed at about 80 MB per worker however many distinct topics arrive.
- Count error under 1% for any topic with more than 2,500 mentions in the window.
- List read: p99 under 50 ms, at 14,000 reads a second.
Step 2 of 6: Size it
The sketch and its baselines are small, and the lists are tiny, so serving is easy. The work is in counting a long tail of topics in bounded memory, scoring against a baseline, and keeping spam off the result.
Assumed for the exercise, not facts about a real system:
- 500 million posts a day with 1.5 topics each on average. The peak is 4 times the average.
- The stream is split into 128 partitions by a hash of the topic. Each partition has one counting worker.
- Each worker keeps a count-min sketch per minute: 5 rows of 65,536 counters, 4 bytes each. It holds 60 minutes and keeps hourly copies for 7 days.
- Each event updates two keys, its region with the topic and worldwide with the topic, so a worker adds 2 counts per event.
- The trending window is 15 minutes. A topic needs at least 300 mentions in it to be listed. 300 million list views a day.
- Topic events per second
- 500M × 1.5 = 750M ÷ 86,400 s ≈ 8,700/s; × 4
- ≈ 8,700/s average, ≈ 34,700/s at the peak
- Sketch memory per worker
- 65,536 × 5 × 4 bytes = 1.31 MB a minute; × 60 minutes
- ≈ 79 MB, fixed
- Over-count in the window
- Error rate 2.718 ÷ 65,536 ≈ 0.0000415. At the peak a worker sees 34,700 ÷ 128 ≈ 271 events/s, 2 counts each: 542 × 60 ≈ 32,500 adds a minute, so ≈ 1.35 a minute; × 15
- ≤ ≈ 20 extra mentions at the peak, with 99% confidence
- Baseline store
- 128 workers × 24 hours × 7 days = 21,504 hourly sketches × 1.31 MB
- ≈ 28 GB
- Dedupe set per worker
- 271/s at the peak × 900 s = 244,000 pairs × 16 bytes
- ≈ 3.9 MB
- Lists in the cache
- 13 lists × 50 topics × 100 bytes; 300M ÷ 86,400 ≈ 3,500 reads/s, × 4
- ≈ 65 KB, ≈ 14,000 reads/s at the peak
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.
Every post produces one event for each topic in it, with the topic, region, user and time. Events go on the Topic stream, partitioned by a hash of the topic. A Count worker owns some partitions, so every event for one topic, from every region, reaches the same worker.
The worker drops banned topics using the Blocklist, looks up the poster in Account signals, counts each user once per topic, and adds the event to its sketch for the current minute under two keys, its region with the topic and worldwide with the topic. It also keeps 13 small heaps of its busiest topics, one for each of 12 regions and one worldwide. Every few seconds it sends them to the Trend scorer.
Once a minute the scorer joins the workers' lists and compares each candidate with what is normal for this hour, read from the Baseline store. It keeps the 50 highest scores for each region and worldwide, and writes them to the Trends cache. The Trends API only reads the cache, so the App never waits on counting.
Other paths through the system
Rank the trends
- Once a minute the scorer joins the lists of all 128 workers. A topic lives on one worker, so all its counts come from one list.
- For each region and topic with at least 300 mentions in 15 minutes, it asks what that pair reached in this hour on each of the last 7 days.
- The baseline returns an average of 200 mentions per 15 minutes for that region and topic, read from the sketch of the worker that owns it.
- It ranks by a smoothed ratio, keeps 50 topics for each of 12 regions and for worldwide, and writes them to the cache with a 2-minute TTL.
Open the trending tab
- A user opens the trending tab, and the app asks for the list for their region.
- The API reads that region's list from the cache.
- The cache returns the 50 topics, with their scores, which are at most a minute old.
- The app shows the top 10 and loads the rest on scroll.
Step 4 of 6: Go deep on the hard part
Counting a long tail in fixed memory
Interviewer asks
How do you find the busiest topics in a stream of 35,000 events a second, in bounded memory?
An exact map from topic to count grows with the number of distinct topics, and nobody controls that number.
Read moreShow less: Counting a long tail in fixed memory
One account can invent millions of junk tags. A count-min sketch keeps memory fixed instead.
- How it works. A sketch is a small grid of counters, here 5 rows by 65,536 columns. Each row has its own hash function. To add a key, hash it once per row and add 1 to one counter in each row. To ask for a count, read the same 5 counters and take the smallest.
- Why the smallest. Other topics collide into the same counters and only ever add to them. So every counter is at least the true count, and the smallest one has had the least interference. A sketch can over-count, but it never under-counts.
- The error, in numbers. With 65,536 columns, each added count can inflate another by about 2.718 ÷ 65,536 ≈ 0.0000415, and with 5 rows the bound holds with probability about 99.3%. At the peak a worker sees about 271 events a second and adds 2 counts for each, about 32,500 a minute. One minute over-counts by at most about 1.35 mentions, and 15 minutes by about 20. A topic at the floor of 300 might read 320. A topic with 4,000 mentions reads at most 0.5% high.
- Memory. 65,536 × 5 × 4 bytes is 1.31 MB for a minute, and 60 minute buckets are about 79 MB per worker. It does not grow with the number of topics.
Region and topic together. The key is the region with the topic, and each event also updates the worldwide key. Without a region in the key, a topic surging in one small region would never reach a worldwide list. Doubling the counts doubles the error, so the sketch is twice as wide.
Sliding window. Keep one sketch per minute. The last 15 minutes is the sum of the last 15 estimates, and a sketch is dropped when it is 60 minutes old. Nothing is ever decremented.
Top K. A sketch answers how many for a topic, but it cannot list topics. So each worker also keeps 13 min-heaps, one per region and one worldwide, of the 200 keys with the highest estimated 15-minute count. On each event it updates the sketch and takes the estimate. If the key is in its heap, it updates it. If not, and the estimate beats the heap's smallest entry, it swaps it in. Once a minute it re-estimates the heap's members, because their window has moved.
Merging. The stream is partitioned by topic, so each topic is counted on exactly one worker. The scorer just joins 128 workers' lists, with no double counting. Partition by post or by user instead, and one topic would be split across many workers, each seeing a part, so merging top lists would miss it. The price is that one very hot topic loads one partition.
Trending, not just popular
Interviewer asks
How do you rank what is rising, rather than what is always big, and avoid a topic with 5 posts topping the list?
The biggest topics by count are much the same every day, such as a big sport or a greeting.
Read moreShow less: Trending, not just popular
People want to see what is unusual, so compare the present with what is normal for the topic.
- Baseline. The expected count is the average of the same clock hour on each of the last 7 days, divided by 4 to match the 15-minute window. Each hour, a worker adds its 60 minute sketches into one hourly sketch and saves it. 128 workers × 24 hours × 7 days is 21,504 sketches of 1.31 MB, about 28 GB. To read a baseline, ask only the sketch of the worker that owns the topic, with the region and topic key. Its error bound is the same 20 mentions.
- The score. Use a smoothed ratio: (current + 50) ÷ (expected + 50). The 50 is a smoothing constant.
- Floor. Only a region and topic with at least 300 mentions in the window can be listed, and each region is scored on its own counts. This removes tiny topics before any ratio is taken, and the sketch error of about 20 is small beside 300.
Three topics show what the smoothing does:
- A marathon topic with 4,000 now and 200 expected scores 4,050 ÷ 250 = 16.2.
- A big sport topic with 60,000 now and 55,000 expected scores 60,050 ÷ 55,050 = 1.09. It is large, but it is not unusual.
- A junk topic with 5 now and 0 expected scores 55 ÷ 50 = 1.1. Without smoothing the ratio would be infinite, and it would top the list.
A topic with no history has an expected count of 0, so it can trend only through a large current count: 3,000 now scores 3,050 ÷ 50 = 61. A bigger smoothing constant favours big topics, and a smaller one favours small ones. Tune it by comparing the lists with what editors would pick.
Stable lists. If the list reshuffles every minute it looks broken. Use two thresholds. A topic enters at a score of 3 or more, and stays until it falls below 2. Then a topic hovering near the line does not flicker in and out.
Quality, spam and serving
Interviewer asks
How do you stop a bot network from steering the list, and serve it to millions of readers cheaply?
Everyone sees this list, and anyone can post, so assume people will try to push topics onto it.
Read moreShow less: Quality, spam and serving
- Dedupe. Count a user once per topic per 15 minutes. The worker keeps a set of hashed user and topic pairs that expire after 15 minutes. At about 271 events a second at the peak that is 244,000 pairs of 16 bytes, about 3.9 MB. A user has one region, so one set covers both keys. The partitioning means the same pair always reaches the same worker, so nothing is shared.
- New accounts. Count mentions in tenths. A new account counts 3 tenths, an established one 10. For the heap topics only, the worker also tracks the share of weight from new accounts. If it passes 60%, the score is cut sharply and the topic goes to review.
- Blocklist. Banned terms are dropped on arrival, and the list is reloaded every minute so a new ban applies within about 60 seconds.
- Hold new topics. A topic that was never seen before and is rising fast is kept off public lists until a classifier or a person has looked at it. A new topic can be a breaking story, but it can also be an attack.
Serving. The scorer builds 13 lists a minute: 12 regions plus worldwide, each from that region's own scores. Each has 50 topics, so the lot is about 65 KB. They go into the cache with a 2-minute TTL, so if one refresh fails the lists are a little stale, not empty. The API only reads the cache. At 14,000 reads a second at the peak, one small cache cluster, or a CDN in front of the API, is plenty. The app can poll once a minute, and no push channel is needed.
If a worker restarts. It rebuilds its sketches by replaying the last 60 minutes from the stream, which keeps events longer than that. Until it catches up, the scorer ignores its list and keeps the previous result for its topics. Use the event's own time, not the arrival time, to pick the minute bucket, so a replay lands in the right place.
Step 5 of 6: Name the trade-offs
Counting
A count-min sketch per minute, with a top-K heap over An exact map from topic to count
Memory is fixed at about 79 MB a worker, and junk tags cannot blow it up. The cost is a small over-count, at most about 20 mentions in the window at the peak.
Partition key
A hash of the topic over A hash of the post or the user
Each topic is counted on one worker, in all its regions, so the lists merge with no error. The cost is that a very hot topic loads a single partition.
Ranking
A smoothed ratio against the same hour on past days over Raw count, or a raw ratio
Rising topics surface, always-big topics sink, and 5 posts cannot spike. The cost is a smoothing constant to tune and about 28 GB of baselines.
Serving
Precompute the lists each minute and cache them over Compute the ranking when a user asks
Reads cost the same however many people ask. The cost is that a list can be up to a minute old.
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 handle one extremely hot topic?
Once the scorer sees a topic is hot, producers add a suffix of hash(user) mod 8 to its key, so its events spread over 8 partitions. Each sub-key is counted separately, and the scorer adds them back together. A user always gets the same suffix, so each user and topic pair still reaches one worker and the dedupe holds. The scorer tells producers which topics are hot from its previous list.
- How would you make trends personal?
Use the shared lists as a pool of candidates, then re-rank them for each user by their language, follows and past taps. Only the final ordering is per user. Counting stays shared, because counting per user is a different and much larger system.
- How do you handle weekday patterns, such as Monday morning news?
Compare with the same weekday over the last 4 weeks in place of the last 7 days. That means keeping 28 days of hourly sketches, about 113 GB instead of 28 GB, which is still modest. Fall back to the 7-day average for topics with no weekly history.
- What if the stream is delayed?
Counts use the event's own time, so late events land in the right minute until that minute's sketch is closed. If the delay is long, the scorer sees older data and keeps serving the last good lists. Alert on the age of the newest event a worker has seen.