News Feed (Twitter/Instagram timeline) system design case. Hybrid push/pull fan-out with celebrity-tier carve-out, per-user Redis ZSET inbox cache, ML ranking pipeline. 5 scenarios cover regular post fanout-on-write, celebrity post skip-push, feed-view cache hit + ranking, cache-miss rebuild from Cassandra, and ranking pipeline reordering. 2 ADRs: push-vs-pull-vs-hybrid trade-off and capped-ZSET + idle-TTL eviction. Capacity hints on every node.
News Feed является классическим кейсом системного дизайна, потому что он выглядит как простой список постов, но внутри требует решений о fan-out, ranking, cache, graph, storage, freshness и celebrity traffic. Пользователь ожидает открыть приложение и за сотни миллисекунд увидеть релевантную ленту. При миллиардах пользователей и десятках миллиардов просмотров ленты в день невозможно каждый раз строить feed прямым запросом по всем друзьям и подпискам.
Кейс полезен тем, что заставляет явно выбрать между fan-out on write, fan-out on read и hybrid. Для обычного пользователя, у которого сотни подписчиков, push в inbox followers дешев и дает быстрое чтение. Для знаменитости с десятками миллионов followers push становится катастрофой: один пост создает миллионы записей в Redis и очередь fanout растет быстрее, чем система успевает ее обработать. Поэтому production-дизайн почти всегда hybrid: обычные авторы push, celebrity-tier pull на чтении.
Этот дизайн связан с ::concept{slug="caching-strategies"}, ::concept{slug="message-queues"}, ::concept{slug="recommendation-system"}, ::concept{slug="sharding"}, ::concept{slug="social-graph"}, ::concept{slug="event-driven-architecture"} и ::concept{slug="back-of-envelope"}. Он также объясняет, почему feed service часто становится центральной точкой интеграции между post storage, graph service, ranking ML и notification system.
Мысленная модель: feed read должен быть быстрым, поэтому система заранее готовит кандидатов. Когда обычный пользователь публикует пост, Post Service сохраняет metadata и media references в durable storage, публикует post.created в Kafka, fan-out workers находят followers через social graph и добавляют post_id в per-user inbox cache, например Redis ZSET с score по времени или initial rank. Когда пользователь открывает ленту, Feed Service берет несколько сотен candidate ids из Redis, подтягивает bodies и counters, вызывает ranking service и возвращает top 20-30 элементов.
Для celebrity authors mental model другой. Fan-out worker помечает событие как celebrity и не пушит его всем followers. Вместо этого Feed Service при чтении смотрит, на каких high-fanout авторов подписан viewer, забирает их свежие посты pull-запросом, объединяет с pushed inbox и отдает ranking pipeline. Так система платит стоимость только за активных readers, а не за всех потенциальных followers.
Data model обычно разделяет posts, media, relationships, feed inbox и engagement. Posts могут жить в Cassandra/DynamoDB-like storage, sharded by author_id или post_id. Feed inbox хранит только ids и lightweight features. Likes/comments counters часто хранятся отдельно и обновляются асинхронно, чтобы не блокировать feed rendering.
Диаграмма показывает ingress через Edge/CDN и API Gateway, write path через Post Service, durable posts storage и Kafka fanout topic, read path через Feed Service, Redis per-user feed cache, social graph, hydration storage и ML ranker. Такое разделение отражает разные требования: write path должен надежно принять пост, read path должен быть очень быстрым, fanout path может быть eventually consistent.
Redis ZSET inbox cache на диаграмме является ключевой оптимизацией. Он не хранит весь контент поста, только candidate ids, timestamp/rank и возможно author metadata. Это уменьшает память и позволяет быстро обрезать список до top N, например 500 или 1000 кандидатов. Capped ZSET плюс idle TTL экономят память на неактивных пользователях.
Ranking service вынесен отдельно, потому что сортировка по времени недостаточна для Facebook/LinkedIn-class feed. Ranker учитывает affinity, recency, media type, engagement prediction, hide/report signals, author quality и diversity rules. При этом он не должен выполнять дорогие graph traversal синхронно на каждый item; heavy features лучше готовить заранее.
Regular post fanout учит fan-out on write. Пользователь с 200 followers публикует пост, событие попадает в Kafka, workers читают followers и добавляют post_id в inbox каждого follower. Стоимость write увеличилась, зато чтение ленты стало быстрым: LRANGE или ZREVRANGE плюс hydration.
Celebrity post skip-push показывает главный масштабный trade-off. Автор со 100M followers не должен породить 100M записей в Redis. Система сохраняет пост, маркирует author как high fanout и оставляет его для pull on read. Пользователь увидит пост, когда откроет feed и ranker решит, что он релевантен.
Feed view cache hit показывает hot path. Feed Service получает candidate ids из Redis, batch-читает posts из storage/cache, подтягивает counters, вызывает ranker и возвращает top items. Цель p95 обычно сотни миллисекунд, поэтому важны batch requests, parallel hydration и bounded candidate set.
Cache miss rebuild учит degradation path. Если inbox пользователя отсутствует из-за idle eviction или cold start, Feed Service строит candidates из последних постов друзей и подписок, возможно с меньшим качеством ranking, заполняет cache и возвращает результат. Miss не должен превращаться в timeout.
Ranking pipeline reorder показывает, что feed не является чистой хронологией. ML может поднять важный пост выше нового, вставить sponsored content, скрыть повторяющиеся posts одного автора и применить privacy filters.
Fan-out on write дает очень быстрые reads, но дорогие writes и backlog при celebrity posts. Fan-out on read дешевле на write path и всегда fresh, но медленнее при каждом открытии ленты. Hybrid сложнее, но обычно единственный практичный вариант для социальной сети с power-law распределением followers.
Redis inbox cache ускоряет ленту, но потребляет много памяти. Если хранить 500 ids по 16-32 bytes на сотни миллионов активных пользователей, счет идет на терабайты. Capping, compression, idle TTL и regional cache placement становятся обязательными.
Strong freshness конфликтует с latency. Если требовать, чтобы новый пост всегда мгновенно появлялся у всех followers, придется платить огромной fanout latency и сложной консистентностью. Большинство продуктов принимают eventual consistency: пост может появиться через секунды, но read path остается быстрым.
ML ranking улучшает engagement, но ухудшает explainability и добавляет dependency. Если ranker недоступен, нужен fallback: chronological ranking или cached scores. Иначе feed outage может случиться из-за ML service, даже если storage и cache живы.
Hydration из одного большого storage проще, но создает hot partitions. Для media-heavy feed часто отделяют post metadata, media CDN, counters и comments preview. Это усложняет сборку response, зато масштабирует компоненты независимо.
Facebook News Feed, LinkedIn feed, Twitter/X timeline, Instagram home feed, TikTok following feed и Reddit home page используют варианты этого паттерна. Twitter долго обсуждал fanout service для home timelines, Facebook известен TAO/social graph и ranking-heavy feed, Instagram комбинирует media storage, CDN и feed generation.
На инфраструктурном уровне встречаются Kafka или Pulsar для post.created, Redis/Memcached для feed cache, Cassandra/DynamoDB/MySQL sharding для posts, TAO-like graph service для friends/follows, Flink/Spark для offline features и отдельные ML serving systems для ranking. Notification system часто подписывается на те же engagement events, но имеет другую цель: не построить feed, а доставить конкретное сообщение пользователю.
Строить ленту синхронным запросом SELECT posts WHERE author_id IN (all_friends) ORDER BY created_at при большом графе. Это работает для учебного MVP, но ломается на миллионах пользователей и celebrity follows.
Фан-аутить celebrity posts всем followers. Power-law распределение делает среднее число followers бесполезным ориентиром: хвост создает основную нагрузку.
Хранить полный post body в feed inbox. Это приводит к массовой денормализации, дорогим invalidation и огромной памяти. Inbox должен хранить candidates, а не весь контент.
Забывать privacy filtering. Если пользователь заблокировал автора или пост стал private, feed должен убрать item даже если его id уже лежит в Redis.
Не иметь rebuild path для cold cache. Redis eviction, региональный failover или новый пользователь должны иметь корректный fallback.
Делать ranking service единственной точкой отказа без fallback. Лента должна деградировать, а не полностью исчезать.
Сложный hybrid feed не нужен продукту с маленькой аудиторией и низкой частотой постов. Для MVP достаточно pull on read из SQL с индексами по author/time и простым cache. Ранний fanout pipeline может быть лишней операционной нагрузкой.
Этот подход не подходит для strict audit feed, где порядок должен быть полностью объяснимым и неизменным, например банковская лента операций. Там нужны deterministic ordering, pagination по ledger cursor и отсутствие ML reorder.
Он также не является заменой search. Feed оптимизирован под персональный поток кандидатов, а не под произвольный full-text query по всему corpus.
Начните с ::concept{slug="back-of-envelope"}: оцените DAU, feed views, posts per day, fanout size и Redis memory. Затем изучите ::concept{slug="message-queues"} и ::concept{slug="event-driven-architecture"}, потому что fanout pipeline асинхронный. Для read path нужны ::concept{slug="caching-strategies"} и ::concept{slug="sharding"}. После этого разберите ::concept{slug="recommendation-system"} и ::concept{slug="social-graph"}. Полезно сравнить этот кейс с notification-system: оба используют fanout, но news feed fanout оптимизирует будущие reads, а notifications fanout оптимизирует доставку по каналам и compliance.