Design problems
Social, feeds & contentMediumSDE-2 30 min

Design a news aggregator (Inshorts / Google News)

Asked at
GoogleInMobiJioHotstarMicrosoftShareChat

The interviewer asks

“Design a news aggregator like Google News or Inshorts. It collects articles from thousands of publishers, groups articles about the same story, and shows each user a fresh, personalised feed, including short summaries in their own language.”

What a strong answer looks like

A strong answer is a pipeline plus a read-heavy serving layer. Ingestion polls RSS feeds and sitemaps (and accepts publisher push) with a per-domain scheduler, fetches and extracts article text, deduplicates near-identical copies, and clusters articles into stories using embeddings. Enrichment adds entities, category, language and, for an Inshorts-style product, a 60-word summary from an LLM with guardrails and human review for sensitive stories. The serving side ranks story clusters by freshness, importance (how many sources cover it) and user interest, and caches heavily, because most users in a region see similar top stories. Breaking news must reach the feed within a few minutes, so freshness is the key non-functional requirement.

Concepts to mention

Adaptive crawl scheduling with conditional GETPer-domain politeness and robots.txtCanonical URL normalisationSimHash near-duplicate detectionEmbedding-based online clustering of storiesMultilingual embeddings for cross-language clustersLLM summarisation with fact checks and human reviewFreshness decay in rankingPrecomputed ranked lists per section/language/regionCDN caching for anonymous traffic

1 Requirements

Functional

  • Ingest articles from ~50,000 publisher sources (RSS, sitemaps, publisher push API)
  • Remove duplicates and group articles about the same event into one story cluster
  • Personalised home feed plus sections: Top, India, Business, Sports, local city
  • Short summaries (about 60 words) and support for 10+ Indian languages
  • Search news by keyword and topic
  • Breaking-news push notifications for major stories

Non-functional

  • Freshness: breaking stories visible within 5 minutes of the first publisher posting
  • 50M DAU, feed p99 under 150 ms
  • 99.9% availability for reads; ingestion may lag briefly
  • Respect publishers: robots.txt, rate limits per domain, link back to source
  • Spikes of 10x during elections, budget day and cricket finals

Out of scope (say it out loud)

  • Paywall and subscription handling
  • Comments and social features
  • Advertising system
  • Video news

2 Estimates

Articles ingested~2M/day (~25/s, peak ~200/s)50k sources × ~40 articles/day on average; big publishers far more
Outbound fetches~150/s average, ~500/s peak50k feeds polled every 1-30 min (adaptive), average ~10 min → 50k ÷ 600 s ≈ 85/s; plus ~25 article pages/s and sitemaps; small fleet, the hard part is politeness, not volume
Storage~20 GB/day text, ~1 TB/year with embeddings2M × ~10 KB clean text and metadata; + 768-dim float embeddings ≈ 3 KB each
Feed reads~20k/s average, ~100k/s peak50M DAU × ~30 feed requests/day (scroll pages, refreshes) ÷ 86,400 ≈ 17k/s; election-day spikes ×5
Summaries (LLM)~100k/daySummarise only clusters with 2+ sources or high-authority sources, not every copy; ~5% of 2M
Active clusters~50k stories at any timeStories older than ~72 h fall out of the main feed

3 API

EndpointWhat it does
GET/v1/feed?section=top&lang=hi&cursor=Personalised list of story clusters with headline, summary, sources, image
GET/v1/stories/{story_id}Full coverage: all articles in the cluster, timeline, related stories
GET/v1/search?q=&from=&lang=Keyword and entity search over recent articles
POST/v1/eventsBatched clicks, reads, dwell time, hides for personalisation
PUT/v1/users/me/preferencesFollowed topics, languages, city, blocked sources
POST/v1/publisher/articlesPublisher push API (or WebSub ping) for instant ingestion

4 Data model

sourcesPostgres
source_id (PK)domainfeed_urlslanguageauthority_scorecrawl_intervallast_fetched_atetag

Small and relational; the scheduler reads due sources. ETag and Last-Modified allow cheap conditional GETs.

articlesCassandra / DynamoDB + S3 for raw HTML
article_id (PK, hash of canonical URL)source_idurltitlebody_textlangpublished_atsimhashembedding_refcluster_id

High write volume, key lookups by article_id; raw HTML archived in S3 for re-extraction.

story_clustersPostgres (hot) + Redis
cluster_idcentroid_embeddingheadlinesummary_by_langarticle_idsentitiescategoryfirst_seenlast_updatedimportance_score

~50k active rows, updated often; served from Redis with the hottest lists precomputed.

search indexElasticsearch / OpenSearch
article texttitleentitieslangpublished_atsource

Time-based indices (one per day) so old ones are cheap to drop or move to warm tier.

vector indexMilvus / pgvector / OpenSearch k-NN
article_idembeddingpublished_at

Only the last ~3 days are needed for clustering, which keeps the index small.

user_profilesRedis / DynamoDB
user_idlanguagescitytopic weightsrecent read cluster_ids

Topic weights updated by a stream job from read events.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALranked storiesdue sourcesconditional GETarticle.extractedindex + kNNnew/changed storysummaryrank job refreshApp / webCDN + API gatewayCrawl schedulerFeed serviceSearch serviceFetch + extractworkersKafka (articles)LLM summariserDedup + clusterworkersRedis (ranked lists)Articles + clusters DBElasticsearch +vectorsPublisher sites / RSS
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Ingest a new article

  1. 1Scheduler picks sources whose next_fetch_at has passed; busy publishers are polled every minute, quiet ones every 30 minutes (adaptive interval)
  2. 2Fetch worker does a conditional GET on the RSS feed (If-None-Match / If-Modified-Since); 304 means nothing new
  3. 3For new items, it fetches the article page, respecting robots.txt and a per-domain rate limit, and extracts main text, title, image, publish time with a boilerplate remover
  4. 4Canonical URL is normalised (strip utm params, follow rel=canonical) and hashed into article_id; if it already exists, stop
  5. 5Article goes to Kafka; dedup worker computes a SimHash and drops near-identical copies (syndicated agency stories like PTI or Reuters appear on hundreds of sites)
  6. 6Clustering worker embeds the article and searches recent clusters (last 72 h) by cosine similarity plus shared entities; joins the best cluster above a threshold, else starts a new one
  7. 7If the cluster is new or changed significantly, the summariser is triggered; the ranking job picks up the change on its next run (every 30-60 s)

Open the app

  1. 1App requests /feed with language and city; the gateway serves a cached response for logged-out users from the CDN (TTL 60 s)
  2. 2Feed service reads precomputed ranked candidate lists from Redis: top-national, top-by-state, top-by-category, per language
  3. 3It merges candidates with the user profile (topic weights, followed topics, recently read clusters) and re-scores ~500 stories
  4. 4It removes stories already read and blocked sources, applies diversity (not 5 cricket stories in a row) and returns 20 stories

7 Deep dives

Crawl scheduling and freshness

Freshness depends on how fast we discover new articles, so the scheduler is the heart of ingestion. Each source has an adaptive interval: if the last few polls found new items, poll sooner (down to 1 min for top publishers); if not, back off (up to 30-60 min). Use conditional GET with ETag and Last-Modified so most polls return a cheap 304. Better than polling is push: support WebSub (PubSubHubbub) and a publisher API so large partners notify us instantly; many Indian news sites also publish Google News sitemaps that list the last 48 hours of articles, which is a dense source. Implement scheduling as a priority queue of (next_fetch_at, source_id), sharded by source so each scheduler owns a slice; Redis sorted sets work well at this scale. Politeness matters: a per-domain token bucket caps concurrent fetches so we never hurt a small publisher's server, and we honour robots.txt and crawl-delay. Freshness SLO is measured as time from publisher's published_at to visibility in our feed, tracked per source tier, with alerts when a top source has not produced anything for an unusual time, which usually means their feed format changed and extraction broke.

Deduplication and story clustering

Two different problems. Duplicate detection finds the same text republished: agency copy from PTI or ANI appears on hundreds of sites with small edits. SimHash over shingles gives a 64-bit fingerprint, and articles within a Hamming distance of about 3 are treated as duplicates; we keep the highest-authority copy as canonical and others as alternate sources. Clustering is harder: different articles about the same event ('RBI keeps repo rate unchanged' from ten outlets in four languages). Use a multilingual sentence embedding of title and first paragraphs, then an online, incremental algorithm: for each new article, find nearest recent cluster centroids with an ANN search limited to the last 72 hours and the same broad category, and join if similarity is above a tuned threshold and they share key entities (people, organisations, places from NER). Otherwise start a new cluster. Online clustering drifts, so a periodic batch job re-merges clusters that have converged and splits ones that grew too broad. Multilingual embeddings let a Hindi and an English article join the same story, which is a big product win in India.

LLM summaries with guardrails

Inshorts-style 60-word summaries used to be written by editors; today an LLM can draft them, but news has high stakes for errors. Summarise per cluster, not per article, and feed the model 3-5 high-authority articles so the summary reflects consensus. Constrain it: a fixed word limit, no opinions, every number and name must appear in the sources. Run a cheap automatic check that extracts entities and numbers from the summary and verifies they appear in the source text; fail means regenerate or send to an editor. Sensitive categories (elections, communal incidents, deaths, legal cases) go to a human review queue before publishing. Translation to regional languages can be done by the same model, with a glossary for names. Cost is controlled because we summarise ~100k clusters a day, not 2M articles, and only regenerate when a cluster changes meaningfully (new facts, not just a new source). Track model, prompt version and source article IDs with every summary so you can audit any complaint. This is an async pipeline: the story appears with the headline first and the summary fills in seconds later.

Ranking and caching the feed

Most of the ranking work is shared across users. A ranking job runs every 30-60 seconds and scores every active cluster on importance (number and authority of sources, how fast new sources are joining, a breaking-news signal), freshness (exponential decay with a half-life of a few hours) and engagement (click-through and dwell in the first minutes). It writes ranked lists to Redis for each (section, language, region) combination, maybe a few thousand lists of ~500 stories each. The per-user step is then cheap: merge a handful of lists, re-weight by the user's topic interests, filter read items, and apply diversity. That keeps p99 well under 150 ms and lets the system survive a 10x spike on budget day because the expensive work does not grow with users. Logged-out traffic is served from the CDN with a 60-second TTL. For breaking news, the ranking job can be triggered immediately when a cluster's source count jumps sharply, and the notification service picks the same event to send pushes.

8 Trade-offs

Polling vs push ingestion

Polling works for every publisher but costs requests and adds delay. Push (WebSub, publisher API) is instant and cheap but only big partners adopt it. Use both: push where available, adaptive polling for the long tail.

Per-user ranking vs shared ranked lists

Fully personal ranking with a large model per request is expensive and adds little for news, where importance is mostly shared. Shared lists re-weighted per user give most of the benefit at a fraction of the cost and survive spikes.

LLM summary vs extractive summary

Extractive summaries (pick the best 2-3 sentences) never invent facts but read poorly and cannot translate. Abstractive LLM summaries read well and can be multilingual but can hallucinate. We pick LLM plus factual checks plus human review for sensitive categories.

Strict vs loose clustering threshold

A loose threshold merges different stories (two separate accidents in different states); a strict one shows the same story five times. Tune per category and use entity overlap as a second check; error on the side of splitting, then batch re-merge.

9 Bottlenecks

Breaking-news burst

A major event makes thousands of publishers post within minutes. Ingestion queues grow; clustering becomes hot on one cluster. Kafka absorbs the burst, workers autoscale on consumer lag, and updates to a single cluster are batched (update centroid and counts every few seconds rather than on every article).

Election-night read spike

Reads jump 5-10x on results day. Shared ranked lists in Redis with replicas, CDN caching for anonymous users, and degrading personalisation (serve section lists directly) keep latency stable.

10 Failure handling

Extractor breaks for a big publisher

Site redesigns break HTML extraction silently. Monitor per-source article yield and text length; on anomaly, fall back to the RSS description and title, and alert the on-call to update the extractor rule.

Summariser produces a wrong fact

Automatic fact checks catch most errors. If a wrong summary goes live, editors can pull it with one click; the cluster falls back to the original headline. Keep audit logs of model and inputs.

Ranking job stalls

Feed service keeps serving the last ranked lists from Redis (they are stale by minutes, not broken). Alert if lists are older than 5 minutes; freshness SLO is breached but availability is kept.

11 What impresses

  • Defining freshness as an SLO measured from publisher time to feed visibility
  • Distinguishing duplicate detection from story clustering
  • Summarising per cluster rather than per article to save cost and improve accuracy
  • Handling Hindi and English articles about the same event in one cluster
  • Moving expensive ranking off the request path into a periodic shared job
  • Monitoring extraction quality per source

12 Mistakes

  • Polling every source at the same fixed interval
  • Treating dedup as exact URL matching only
  • Calling an LLM synchronously on the read path
  • No guardrails or review on AI summaries of sensitive news
  • Fully per-user ranking that collapses during election-day spikes
  • Ignoring publisher politeness and robots.txt

13 Follow-ups

Q1How would you detect a breaking story and send push notifications without spamming users?
Q2How do you handle an article that gets updated or retracted after publishing?
Q3How would you add a 'local news for my district' section?
Q4How do you avoid filter bubbles and show balanced sources?
Q5How would you evaluate the quality of the clustering?
Q6How do you deal with fake news or low-quality sources?

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