Design a news aggregator
Collect articles from thousands of publishers and show each reader a fast, endless, up-to-date feed for their region. It has two halves with opposite characters: a background ingestion pipeline that's write-light and can take minutes, and a feed that's read-heavy, must load in under 200 ms, and must survive the whole world refreshing at once when big news breaks. It follows the interview framework phase by phase.
1. The prompt
"Design Google News: aggregate articles from publishers worldwide into a feed per region; clicking an article opens it on the publisher's site."
Ingestion is small: about 12 articles a second. The feed is the challenge: thousands of reads a second normally, a million a second during breaking news, with fresh results. The design separates the two halves and makes the read side almost entirely cached.
2. Frame the problem
Where do articles come from?
RSS/Atom feeds from most publishers, webhooks from a few partners, some sites with no feed at all.
Ingestion needs polling tiers, push support, and maybe a small crawler.
Personalised feeds, or one per region?
One feed per region to start; light personalisation later.
A per-region feed is tiny and shared by everyone, so it can be precomputed and cached.
How fresh?
New articles within ~30 minutes; within a few minutes for major publishers.
Polling intervals per publisher tier.
How big are the spikes?
From 100M daily readers to 500M during breaking news, many refreshing every few seconds.
The read path must never touch the database; CDN and cache replicas take the load.
Do we host the articles?
No: titles, summaries and thumbnails; the click goes to the publisher.
We store small records and thumbnails, not article bodies.
Is a slightly stale feed acceptable?
Yes, a few seconds or minutes. An error page isn't.
Availability over consistency on the read path.
3. What it must do
- The system ingests new articles from publishers continuously.
- A reader sees a feed of recent articles for their region (title, summary, thumbnail, publisher), with infinite scroll.
- Clicking an article opens the publisher's page (and we count the click).
Not now:
- Deep personalisation: a light version appears in 9.6.
- Saving and sharing.
- Comments.
- Clustering duplicate stories: mentioned in follow-ups.
4. How well it must do it
| Target | Why this number |
|---|---|
| Fast: the feed loads in under 200 ms speed | It's the front page. |
| Spike-proof: 1M+ feed requests a second during breaking news load shape | Big news is exactly when people come. |
| Fresh: articles appear within ~30 minutes, within ~5 for major publishers speed | An aggregator that's an hour behind is useless. |
| Available over consistent: a slightly stale feed beats an error consistency or availability | Nobody notices seconds of staleness; everyone notices errors. |
| Polite: polling uses conditional requests and respects publishers' limits rules | Publishers are partners; hammering them gets you blocked. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Feed reads | 100M readers × 5 loads a day ÷ 10⁵ | ~5,000/s normally; 1M+/s in spikes → so cache and CDN, never the database |
| Articles | 50,000 publishers × ~20 articles a day | 1M a day ≈ 12/s → so ingestion load is tiny |
| Feed size | Each region's latest ~2,000 articles × ~1 KB | ~2 MB per region → so every region's feed fits in memory many times over |
| Polling | 50,000 publishers every 60 min | ~14 polls a second; cheap with conditional requests |
6. The nouns
- Publisher: with its feed URL and polling tier.
- Article.
- Regional feed: the precomputed list per region.
- Thumbnail.
publishers publisher_id, name, feed_url, region, tier (fast | normal | slow), etag, last_modified, next_poll_at
articles article_id (time-ordered), publisher_id, url (unique), title, summary, thumbnail_key, region, category, published_at
feed:{region} sorted set of article_id (latest 2,000), with category in each entry
7. The contract
GET /v1/feed?region=IN&category=tech&cursor=<article_id>&limit=20 → {articles: [...], next_cursor}
GET /a/{article_id} → 302 to the publisher's URL (logs the click)
POST /v1/publishers/{id}/webhook (partners push new articles; signed)
Data flow: (1) poll or receive from a publisher → (2) parse, dedupe by URL, store the article → (3) download and resize its thumbnail → (4) change data capture adds it to the regional feed → (5) readers get the feed from the cache or CDN.
8. First working version
Ingestion workers, an articles table, and a feed service that queries it.
8.1 Ingesting articles
A scheduler hands publishers to workers when they're due. A worker fetches the feed, parses new items, skips URLs it has already stored, and inserts the rest.
8.2 Reading the feed
The feed service runs SELECT … WHERE region = ? ORDER BY published_at DESC LIMIT 20 OFFSET ? and returns titles with publisher-hosted image URLs.
8.3 Clicking through
A redirect endpoint logs the click and sends a 302 to the publisher.
Parked for later (section 9 fixes each)
- Offset pagination shows duplicates and gaps as news keeps arriving.
- A database query per feed load collapses during a spike.
- Polling everyone equally is either too slow for big publishers or too wasteful for small ones.
- Hotlinked publisher images are slow and break.
- Breaking-news traffic in one region.
- Categories and personalisation.
9. Make it hold up
9.1 Infinite scroll while news keeps arriving fast
| Approach | Why | |
|---|---|---|
| ✗ Bad | Page numbers or offsets | New articles shift every page: readers see duplicates and miss stories (Module 07). |
| ~ Good | A cursor on published_at | Stable, but articles sharing a timestamp are skipped or repeated at page edges. |
| ✓ Great | A cursor on a time-sortable unique article id (Snowflake-style, Module 20) | Unique and ordered: WHERE id < cursor ORDER BY id DESC LIMIT 20, or a range read on the sorted set. |
9.2 A feed in under 200 ms at a million requests a second fast · spike-proof
| Approach | Why | |
|---|---|---|
| ✗ Bad | Query the articles table on every load | Fine at 5,000/s with an index; fails during a spike. |
| ~ Good | Cache each region's feed with a 30-minute TTL | Fast, but stale for up to 30 minutes, and a stampede when the hot key expires. |
| ✓ Great | Precomputed regional feeds: when an article row is inserted, change data capture (Module 13) emits an event; feed workers add it to the region's sorted set (ZADD feed:IN) and trim to the latest 2,000. Reads are a range on the sorted set; the first page is also cached at the CDN for a few seconds. | Fresh within seconds, no expiry stampedes, and reads never touch the database. |
9.3 Getting articles in quickly fresh · polite
- Polling RSS/Atom feeds in tiers: major publishers every 5 minutes, others every 30 minutes to a few hours, using
ETagandIf-Modified-Sinceso unchanged feeds cost almost nothing (a 304 response). - Webhooks for partners who call you when they publish: seconds of delay, far fewer requests. Keep polling as a fallback.
- Scraping sites without feeds: a last resort, brittle and subject to the site's terms; it's essentially a small crawler (Design D09).
- Ingestion jobs run per publisher on a worker pool, with retries and backoff; a failing publisher doesn't hold up the others. Try the trade-offs in section 13.
9.4 Thumbnails fast
Don't hotlink publisher images: they're slow, vanish and change. Download each image once, store resized versions in object storage, and serve them through a CDN in several sizes (srcset). The CDN absorbs nearly all image traffic.
9.5 Breaking news spike-proof · available
When a huge story breaks, traffic multiplies within minutes. The feed service is stateless and autoscales; regions are separate stacks, so a spike in one doesn't hurt others. The regional sorted set is tiny, so throughput (not memory) is the limit: add read replicas of the cache, and let the CDN serve the first page for a few seconds at a time. Being a few seconds stale is invisible; being down is not.
9.6 Categories and light personalisation fast
- Categories: store the category in each feed entry, read ~1,000 entries and filter in memory (milliseconds), rather than maintaining hundreds of separate sorted sets.
- Light personalisation: keep a small preference vector per user (say 60% technology, 30% business, 10% sport) and build the feed at read time by mixing from category lists, plus trending stories. Far cheaper than precomputing a feed per user, at the cost of coarser personalisation.
9.7 The whole design after the deep dives
An article from a partner's webhook reaching a reader's feed, through every part.
10. Interviewer follow-ups
The same story appears from 40 publishers.
Cluster similar articles: compute text embeddings or shingles for each title and summary, group near-duplicates published close in time, and show one story card with "and 39 more sources". Run clustering as a stream job; feed entries reference the cluster.
A publisher's feed goes from 20 to 2,000 items a day overnight.
Probably spam or a broken feed. Cap items per publisher per hour into the feed, flag anomalies, and quarantine until reviewed.
Traffic grows 10×.
More regional stacks and cache replicas; the CDN does most of the work. Ingestion barely changes. Story clustering becomes worth doing because feeds get noisier.
A publisher asks us to remove an article.
Mark it removed in the articles table; change data capture removes it from regional feeds and the CDN is purged for the affected pages. The thumbnail is deleted from storage.
Show "trending now".
Count clicks per article per region in time buckets (Design D17/D20 style) and keep a top-K list updated every minute.
How do you know ingestion is healthy?
Track per-publisher freshness (time since the last new article, compared with its normal rate), poll errors, and the end-to-end delay from publish time to feed entry for top publishers.
Paywalled publishers?
Show what they allow in their feed (title and snippet), mark the card, and respect their terms. The click still goes to their site.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Feed cache node fails | Nothing | Replica health | Replicas serve; CDN covers the first page |
| CDC pipeline stalls | Feed stops updating | Freshness alert (newest article age) | Feed keeps serving stale; catch up when CDC resumes |
| A publisher's feed breaks | Its articles stop appearing | Per-publisher freshness | Backoff, alert, keep others unaffected |
| Breaking-news spike | Nothing, ideally | Request rate per region | CDN, cache replicas, autoscaling, regional isolation |
| Image storage slow | Missing thumbnails | Image errors | CDN cache; placeholder images |
12. Wrap-up and the clock
Trade-offs we made:
- Separate ingestion and feed services.
- Precomputed regional feeds via CDC (fresh, never expiring) over TTL caches.
- Id cursors.
- Polling tiers plus webhooks (freshness against politeness).
- Read-time category filtering and light personalisation.
What we'd watch:
- Feed p99.
- CDN hit rate.
- Newest-article age per region.
- Per-publisher freshness.
- Poll errors.
At 10×:
- More regional stacks and replicas.
- Story clustering.
A 45-minute interview
| 0–4 | Frame the problem (sources, regional vs personal, spikes) |
| 4–9 | Must do, how well, numbers |
| 9–13 | Nouns, contract, data flow |
| 13–20 | First working version, parked list |
| 20–40 | Deep dives: pagination (9.1), precomputed feeds (9.2), ingestion (9.3), spikes (9.5) |
| 40–45 | Follow-ups and wrap-up |
A 60-minute interview
| 0–5 | Frame the problem |
| 5–11 | Must do, how well, numbers |
| 11–16 | Nouns, contract, data flow |
| 16–24 | First working version |
| 24–50 | Deep dives 9.1–9.6 and the final picture |
| 50–60 | Follow-ups: clustering, trending, takedowns; wrap-up |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Ingestion workers, an articles table, a feed API with pagination, thumbnails in object storage behind a CDN. |
| Senior | Cursor pagination done right, precomputed regional feeds via CDC, polling tiers and webhooks, spikes handled with caches, replicas and regional stacks. |
| Staff+ | Personalisation that scales, story clustering, publisher relationships and legal constraints, cost and freshness trade-offs stated with numbers. |
Common mistakes:
- Offset pagination.
- Querying the database per feed load.
- TTL caches that stampede.
- Polling everyone at the same rate.
- Hotlinking images.
- One global stack.
13. Try it, check yourself, read more
How quickly do new articles appear, and how many requests does ingestion make? Mix polling tiers and webhooks.
Model: an article is published at a random moment between polls, so on average it waits half the interval; webhook publishers notify within seconds. Articles are weighted toward top publishers (they publish ~20× more). Webhook publishers aren't polled.
What's the best cursor for an infinite news feed?
How does the feed stay fresh without stampedes?
During a breaking-news spike, what limits the regional feed cache?
What makes polling 50,000 feeds cheap?
| Resource | Why |
|---|---|
| HTTP conditional requests (RFC 9110) | ETags and If-Modified-Since, which make frequent polling cheap. |
| Design Google News (Hello Interview) | Another walkthrough. |