Design a news aggregator (Inshorts / Google News)
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
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
3 API
| Endpoint | What 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/events | Batched clicks, reads, dwell time, hides for personalisation |
PUT/v1/users/me/preferences | Followed topics, languages, city, blocked sources |
POST/v1/publisher/articles | Publisher push API (or WebSub ping) for instant ingestion |
4 Data model
sourcesPostgressource_id (PK)domainfeed_urlslanguageauthority_scorecrawl_intervallast_fetched_atetagSmall and relational; the scheduler reads due sources. ETag and Last-Modified allow cheap conditional GETs.
articlesCassandra / DynamoDB + S3 for raw HTMLarticle_id (PK, hash of canonical URL)source_idurltitlebody_textlangpublished_atsimhashembedding_refcluster_idHigh write volume, key lookups by article_id; raw HTML archived in S3 for re-extraction.
story_clustersPostgres (hot) + Rediscluster_idcentroid_embeddingheadlinesummary_by_langarticle_idsentitiescategoryfirst_seenlast_updatedimportance_score~50k active rows, updated often; served from Redis with the hottest lists precomputed.
search indexElasticsearch / OpenSearcharticle texttitleentitieslangpublished_atsourceTime-based indices (one per day) so old ones are cheap to drop or move to warm tier.
vector indexMilvus / pgvector / OpenSearch k-NNarticle_idembeddingpublished_atOnly the last ~3 days are needed for clustering, which keeps the index small.
user_profilesRedis / DynamoDBuser_idlanguagescitytopic weightsrecent read cluster_idsTopic weights updated by a stream job from read events.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Ingest a new article
- 1Scheduler picks sources whose next_fetch_at has passed; busy publishers are polled every minute, quiet ones every 30 minutes (adaptive interval)
- 2Fetch worker does a conditional GET on the RSS feed (If-None-Match / If-Modified-Since); 304 means nothing new
- 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
- 4Canonical URL is normalised (strip utm params, follow rel=canonical) and hashed into article_id; if it already exists, stop
- 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)
- 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
- 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
- 1App requests /feed with language and city; the gateway serves a cached response for logged-out users from the CDN (TTL 60 s)
- 2Feed service reads precomputed ranked candidate lists from Redis: top-national, top-by-state, top-by-category, per language
- 3It merges candidates with the user profile (topic weights, followed topics, recently read clusters) and re-scores ~500 stories
- 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
Lessons this design uses
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.