Design problems
Search & discoveryMediumSDE-2 30 min

Design trending topics (top-K heavy hitters)

Asked at
GoogleLinkedInMetaShareChat

The interviewer asks

“Design a "Trending now" feature like Twitter/X trends: show the top 10 trending hashtags or topics globally and per country/city, updated every minute. The input is a firehose of posts.”

What a strong answer looks like

A strong answer first defines 'trending': not the most frequent (that is always #cricket or 'good morning') but the topics whose current rate is much higher than their normal rate. The pipeline is a stream: posts enter Kafka, a stream processor (Flink) extracts and normalises topics, counts them in sliding or tumbling windows per region, and compares them with a baseline to produce a trend score. Exact counting for every topic is fine for hashtags with partitioning by topic, but for unbounded keys the candidate should mention Count-Min Sketch plus a min-heap or the Space-Saving algorithm, with two-stage aggregation (local top-K then global merge) to avoid hot partitions. Results are written to Redis per region every minute and served from cache. Spam and coordinated manipulation filters, plus a human/policy override, complete the design.

Concepts to mention

Trending = growth vs baseline, not raw countStream processing with windowed aggregation (Flink)Count-Min Sketch + min-heap / Space-SavingHyperLogLog for distinct usersTwo-stage aggregation for hot keysEvent time and watermarksPrecomputed results in Redis/CDN for readsAnti-manipulation weighting and blocklists

1 Requirements

Functional

  • Show top 10-30 trending topics globally, per country and per major city
  • Update trends at least every minute
  • Trending means unusual growth, not just high volume
  • Show approximate post counts ("25.3K posts") for each trend
  • Filter spam, bots and policy-violating topics; allow manual removal
  • Merge variants of the same topic (#INDvsAUS, #IndvAus, 'india vs australia')

Non-functional

  • Ingest ~10k posts/s average, ~100k posts/s peak (big match final, elections)
  • End-to-end freshness under 2 minutes from post to trend
  • Read path p99 under 50 ms, very high read QPS (trends shown on every home load)
  • Availability 99.9% for reads; stale trends are acceptable during a pipeline outage
  • Approximate counts are acceptable (within ~1-2%)

Out of scope (say it out loud)

  • Personalised trends ('for you' trends) beyond region
  • Search over trend posts
  • Long-term analytics dashboards

2 Estimates

Ingest rate~6k posts/s avg, ~100k/s peak500M posts/day / 86,400 ~ 5.8k/s; events like an India-Pakistan match spike 10-20x
Topic events~20k/s avg, ~300k/s peak~3 extracted topics per post (hashtags + entities)
Distinct topics per hour~10-50MLong tail: most hashtags appear once; the head is a few thousand topics
Exact counter memory~5 GB per hour window50M keys x ~100 bytes (key + counters) ~ 5 GB; feasible when partitioned across ~20 Flink tasks
Count-Min Sketch memory~2-5 MB per region per windowwidth 2^18 x depth 5 x 4-byte counters ~ 5 MB; error ~ e/width x N ~ 0.001% of total stream
Read QPS~50k/sTrends widget on home load for ~200M DAU x a few loads/day; served from Redis/CDN cache

3 API

EndpointWhat it does
GET/v1/trends?region={country_or_city}&limit=20Return current trends: [{topic, display_name, post_count, rank, category}] with generated_at
POST/internal/trends/blocklistTrust & safety removes or blocks a topic from trends
GET/internal/trends/debug?topic={t}&region={r}Show the counts, baseline and score history for a topic (for ops and appeals)

4 Data model

post_eventsKafka topic (partitioned by post_id)
post_iduser_idtexthashtags[]langgeo (country, city)created_atuser_trust_score

Firehose from the posting service. Retained ~3 days for replay.

topic_eventsKafka topic (partitioned by hash(topic, region))
topic_normregionuser_idtsweight

Output of extraction. Partitioning by topic sends all counts for a topic to one counter task; hot topics need pre-aggregation (see deep dive).

window_countsFlink keyed state (RocksDB)
(topic, region, minute_bucket) -> countdistinct_users (HyperLogLog)

Kept for ~24 h to compute baselines; checkpointed to S3 for recovery.

topic_baselineFlink state or Redis
(topic, region) -> EWMA of ratevariancelast_seen

Expected rate for the same time of day; used to compute the trend score.

trends_currentRedis (sorted set per region) + CDN
trends:{region} -> ZSET topic -> score; trends_meta:{region} -> JSON list with counts and generated_at

Written atomically once per minute (write new key, then swap/rename). Reads never touch the stream pipeline.

trend_historyCassandra / ClickHouse
regionminutetopicrankscorecount

For debugging, appeals and analytics.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALcreate postpost eventkeyed by topicper-minute countscheckpointtop-K every 60 sGET /trendsblocklistPosting clientsReaders (home screen)API gateway + CDNPost serviceTrends APIKafka: posts firehoseFlink: score + top-KFlink: extract +filterFlink: windowedcountsTrend history(ClickHouse)Redis trends perregionCheckpoints (S3)Trust & safety tools
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

A post becomes part of a trend

  1. 1User posts 'What a catch! #INDvsAUS #Kohli'; the post service stores it and publishes an event to Kafka.
  2. 2Flink extraction job parses hashtags and named entities, normalises them (lowercase, strip accents, alias table maps '#IndvAus' and 'india vs australia' to one topic ID) and drops posts from low-trust or bot-flagged accounts.
  3. 3It emits (topic, region, user_id) for the city, the country and global, so one post counts in three region scopes.
  4. 4Counting job, keyed by (topic, region), adds the event to a 1-minute tumbling bucket and to a HyperLogLog of distinct users.
  5. 5Every minute the scoring job computes, for each topic with enough volume, the rate over the last 5-15 minutes vs its baseline, using distinct users rather than raw posts.
  6. 6Each scoring task keeps a local top-K by score; a final single-parallelism task merges local lists into the global top 30 per region.
  7. 7Results pass the blocklist and policy classifier, then are written to Redis as trends:{region} with an atomic swap; history goes to ClickHouse.

Read trends

  1. 1App calls GET /v1/trends?region=IN-MH (Mumbai).
  2. 2CDN or Trends API in-memory cache answers (TTL 30-60 s); on a miss, the API reads trends_meta:IN-MH from Redis.
  3. 3If the city has too little data, the API falls back to the country list.
  4. 4Response returns topics with rounded counts; the app refreshes on the next home load.

7 Deep dives

Defining 'trending' with a score

If you rank by raw count, the list never changes: #goodmorning, #cricket and #bollywood would trend every day. Trending is about acceleration. A simple, explainable score compares the current rate with an expected rate: score = (current_count - expected) / sqrt(expected + c), a z-score-like value, where current_count is distinct users in the last 10 minutes and expected comes from an EWMA baseline for the same topic, ideally for the same hour of the day and day of week. The constant c and a minimum volume threshold stop a topic going from 1 to 5 mentions from scoring infinitely. Use distinct users, not posts, so one account spamming 1,000 posts counts once. Add decay so a trend that peaked an hour ago slides down even if counts stay high. For brand-new topics with no baseline, use a category-level prior. Tune parameters by replaying past days and checking whether real events (a wicket, an election result) surface within a few minutes.

Counting at scale: exact vs sketches

With partitioning by (topic, region), exact counts in Flink keyed state are often fine: 50M keys per hour spread over 20+ tasks is a few GB in RocksDB. Sketches matter when memory per task is limited or keys are truly unbounded. A Count-Min Sketch is a 2D array of counters (depth d hash functions x width w); each event increments one counter per row and the estimate is the minimum across rows. It never undercounts and overcounts by at most about e/w x total events with high probability, using a few MB. A CMS alone cannot list the top items, so pair it with a min-heap of size K: after each update, if the estimated count beats the heap minimum, insert it. Space-Saving (Misra-Gries family) is an alternative that directly keeps K counters and has error bounds. In the interview, say you would start exact with partitioned state and move to CMS + heap or Space-Saving for the long tail, and that approximate counts are acceptable because users see '25K posts'.

Hot keys and two-stage aggregation

Keying the counter job by topic means a single topic like #INDvsPAK during a final can send 50k events/s to one task, which then falls behind and delays every trend in that partition. The fix is two-stage aggregation. Stage one: each extraction task pre-aggregates locally for a short time (e.g. 1-5 seconds) and emits (topic, region, partial_count, partial_HLL) instead of one event per post; this alone cuts traffic by 100-1,000x for hot topics. Alternatively, salt the key ((topic, salt 0-15)) so a hot topic spreads across 16 tasks, then a second stage combines the 16 partial counts. HyperLogLogs merge cleanly, which is why they work for distinct users across stages. Top-K follows the same pattern: each task computes a local top-K, and one merger task combines them; this is correct as long as each topic's full count lives in exactly one task after the second stage.

Manipulation and safety

Trends are a target: political campaigns, crypto spam and bot farms try to push hashtags. Defences are layered. At input, weight events by account trust (age, verified phone, past behaviour) and drop known bots. In scoring, count distinct users and look at diversity: a topic driven by 2,000 accounts created last week that all follow each other is suspicious; network-diversity features can down-rank it. Duplicate-text detection catches copy-paste campaigns. At output, run a policy classifier on trend names, check a blocklist, and give trust & safety a tool to remove a trend with an audit log. In India, also handle region-specific legal takedown orders. Store the score history so you can explain why a topic trended during an appeal or audit.

8 Trade-offs

Exact counting vs approximate sketches

Exact counts are simple to explain and debug and fit in partitioned state at moderate scale. Sketches (CMS, Space-Saving, HLL) save memory and network but add error. Use HLL for distinct users always; use CMS/Space-Saving for the long tail only if memory demands it.

Tumbling vs sliding windows

True sliding windows are expensive. Use 1-minute tumbling buckets and sum the last N buckets for a 'sliding' view; it is cheap and accurate to one minute.

Stream processing vs periodic batch

A batch job every 5 minutes on the data lake is simpler but misses the 2-minute freshness target and wastes work re-reading data. Streaming with Flink keeps state incrementally; Kafka retention allows replay if the job needs a fix.

Push trends to clients vs pull

Pull with short caching is enough: trends change once a minute and are the same for everyone in a region, so a CDN/Redis cache absorbs reads cheaply.

9 Bottlenecks

Hot topic during a big event

Single key overload in the counting job. Local pre-aggregation and key salting with a second merge stage, as above.

Too many regions

Each post counts in city, state, country and global scopes, multiplying events by ~4. Compute only for regions with enough users; roll up smaller cities into states.

Read spikes

Everyone opens the app when a match ends. Trends are identical per region, so serve from CDN with a 30-60 s TTL; Redis sees only cache misses.

10 Failure handling

Flink job crashes

Flink restores state from the last checkpoint (every ~30 s) in S3 and replays Kafka from the stored offsets, so counts stay consistent. Meanwhile Redis keeps serving the last published trends with their generated_at timestamp.

Kafka consumer lag grows

Trends get stale. Alert on lag and freshness; the scoring job uses event time with watermarks so late events are counted in the right minute bucket. Drop events later than a few minutes rather than block.

Bad trend (offensive or manipulated) published

Trust & safety blocklist takes effect on the next read via the Trends API filter, without waiting for the pipeline. All removals are logged.

11 What impresses

  • Defining a trend score formula with a minimum volume and decay
  • Counting distinct users instead of posts
  • Explaining CMS error bounds and why it never undercounts
  • Local top-K plus global merge, and why the merge is correct
  • Separating the write pipeline from a cached read path
  • Planning for manipulation and appeals with score history

12 Mistakes

  • Ranking by total count, so the same topics trend forever
  • Running a GROUP BY on the posts database every minute
  • Using a single counter node that becomes a hot spot
  • Using a Count-Min Sketch without a way to extract the top items
  • Ignoring bots and coordinated campaigns
  • Computing trends on every read request

13 Follow-ups

Q1How would you make trends personalised per user?
Q2How would you show trends for the last 24 hours as well as right now?
Q3How do you merge different spellings and languages of the same topic?
Q4What happens to counts when the Flink job restarts?
Q5How would you detect a coordinated hashtag campaign?
Q6How would you test changes to the scoring formula?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free