MÓDULO 14 · CONCEITO 04 DE 12

Twitter Timeline — fan-out, celebridades e consistência eventual

O problema de timeline expõe o trade-off mais fundamental entre fan-out on write e fan-out on read. Leituras são 100x mais frequentes que escritas — mas fan-out on write explode para celebridades com 100M seguidores. A solução híbrida do Twitter real: fan-out on write para usuários comuns, fan-out on read para celebridades, merge na borda. Redis Sorted Set como timeline cache. Follow graph e backfill.

Tempo de leitura ~30 min Pré-requisito 03 · URL Shortener · 02 · Capacity Estimation Próximo 05 · Ride-sharing Matching →

Em 2012, o Twitter tinha um problema chamado internamente de "Bieber problem". Justin Bieber tinha 30 milhões de seguidores na época, e cada tweet seu disparava 30 milhões de writes simultâneos nos sistemas de timeline — travando o serviço. A solução encontrada foi um dos exemplos mais didáticos de como a escala transforma um problema simples num problema de distributed systems: não dá para tratar um usuário com 30 milhões de seguidores da mesma forma que um com 300.

O Twitter timeline — a lista de tweets das pessoas que você segue, em ordem cronológica reversa — parece simples à primeira vista. Mas ele encapsula o trade-off central de sistemas de feed em qualquer escala: o custo do trabalho deve ser pago no momento da escrita (quando o tweet é criado) ou no momento da leitura (quando o feed é carregado)? A resposta correta é: depende de quem está escrevendo.

Requisitos e estimation

Requisitos funcionais:

# Estimation para escala do Twitter (2023)
# DAU: 250M | MAU: 500M
# Tweets por dia: 500M (~5.800 tweets/s | pico: ~15k tweets/s)
# Leituras de timeline: 250M DAU × 10 checks/dia = 2,5B leituras/dia
#                       → ~29.000 reads/s | pico: ~80k reads/s

# Razão read:write ≈ 5:1 (2,5B leituras ÷ 500M escritas)
# Mas o custo por operação é muito diferente:
#   - Write de tweet: O(1) — inserir 1 registro
#   - Read de timeline sem cache: O(seguidores médios) = O(~200) queries
# Custo efetivo: reads dominam o sistema por uma margem enorme

# Storage de tweets:
# 500M tweets/dia × 500 bytes (texto + metadados) = 250 GB/dia
# Em 5 anos: ~456 TB (só tweets)
# Com replicação 3×: ~1,4 PB — requer armazenamento distribuído

Fan-out on read (pull model)

A abordagem mais intuitiva: quando o usuário abre o timeline, buscar tweets de todos que ele segue e montar o feed na hora.

-- Naive fan-out on read
SELECT t.*
FROM tweets t
JOIN follows f ON t.user_id = f.followee_id
WHERE f.follower_id = :user_id
  AND t.created_at > :cursor
ORDER BY t.created_at DESC
LIMIT 50;

-- Problema: essa query faz um join entre duas tabelas grandes
-- follows: 500M usuários × média de 200 follows = 100B linhas
-- tweets:  500M/dia × 365 × 5 = ~912B tweets
-- Essa query em produção, sem cache agressivo, é proibitiva

Mesmo com índices e otimizações, o problema fundamental permanece: se você segue 1.000 contas, a query precisa considerar 1.000 fontes de tweets e fazer o merge/sort. Em 80k reads/s no pico, isso se multiplica para uma carga insuportável no banco.

Quando fan-out on read faz sentido: para user timelines (tweets de uma única conta) — aí é uma query simples por user_id, com índice e cache por usuário. O problema é específico ao home timeline, onde múltiplas fontes precisam ser combinadas.

Fan-out on write (push model)

A abordagem inversa: quando um tweet é criado, imediatamente inserir uma referência a ele no feed pré-computado de cada seguidor. A leitura do timeline vira uma operação trivial — só buscar do feed pré-computado.

# Estrutura do timeline feed no Redis (Sorted Set por usuário)
# Score = timestamp do tweet (permite ordenação cronológica eficiente)
# Member = tweet_id (referência, não o conteúdo)

ZADD timeline:{follower_id} {tweet_timestamp} {tweet_id}

# Leitura do timeline: busca paginada em O(log N) + O(K)
ZREVRANGE timeline:{user_id} 0 49  # 50 tweets mais recentes
# → retorna lista de tweet_ids

# Busca de conteúdo: batch fetch dos tweets
GET tweet:{tweet_id_1}
GET tweet:{tweet_id_2}
... (pipeline Redis, ~1ms para 50 tweets)

# Fluxo completo de leitura:
# 1. ZREVRANGE timeline:{user_id} → lista de tweet_ids    (~0.5ms)
# 2. MGET tweet:{id1} tweet:{id2} ...                     (~1ms)
# 3. Merge com tweets de celebridades (explicado abaixo)  (~0.5ms)
# Total: ~2ms — latência excelente

A manutenção da timeline como sorted set resolve elegantemente vários problemas: inserção de tweet de quem você segue é um ZADD, remoção de tweet é um ZREM, follow de uma conta é um backfill dos tweets recentes dela, unfollow é um ZREM de todos os tweets dela.

O problema: ao criar um tweet, é necessário executar um ZADD para cada seguidor. Para um usuário comum com 300 seguidores, isso é 300 writes — aceitável. Para Katy Perry com 100M seguidores, isso é 100M writes por tweet. Com o Redis rodando a 100k ops/s, fan-out de um único tweet de celebridade levaria 1.000 segundos — claramente inviável.

O celebrity problem — análise quantitativa

# Distribuição de seguidores no Twitter (lei de potência / long tail):
# ~99% dos usuários têm < 10.000 seguidores
# ~0.9% têm entre 10k e 1M seguidores
# ~0.1% têm mais de 1M seguidores (celebridades)

# Custo de fan-out por tweet:
# Usuário comum (300 seguidores):    300 ZADD = ~3ms de trabalho do fan-out service
# Usuário médio (10k seguidores):  10.000 ZADD = ~100ms
# Influencer (1M seguidores):    1.000.000 ZADD = ~10 segundos
# Celebridade (100M seguidores):100.000.000 ZADD = ~17 minutos

# Se múltiplas celebridades postam simultaneamente (evento ao vivo):
# 10 celebridades × 50M seguidores = 500M writes simultâneos
# → impossível de executar em tempo real

# Conclusão: fan-out on write puro não é viável para celebridades.
# A solução precisa de um tratamento assimétrico baseado no número de seguidores.

A solução híbrida — o que o Twitter realmente fez

O Twitter introduziu um threshold de celebridade (internamente chamado de "heavy hitter threshold") em torno de 1 milhão de seguidores. Abaixo do threshold: fan-out on write. Acima: fan-out on read. O timeline de um usuário é então a combinação das duas fontes:

# Ao carregar o home timeline do usuário U:

# Fonte 1: feed pré-computado (fan-out on write)
# Contém tweets de todas as contas regulares que U segue
timeline_ids = ZREVRANGE timeline:{U} 0 199  # últimas 200 entradas

# Fonte 2: tweets recentes de celebridades seguidas por U
# (fan-out on read — só para celebridades)
celebrity_follows = GET celebrity_follows:{U}  # lista de celeb_ids seguidos por U
# Para cada celeb:
for celeb_id in celebrity_follows:
    celeb_tweets = ZREVRANGE user_tweets:{celeb_id} 0 19  # últimos 20 tweets da celeb
    timeline_ids.extend(celeb_tweets)

# Merge e sort por timestamp (feito no application layer)
# Os tweet_ids carregam o timestamp no Snowflake ID → sort sem fetch do conteúdo
all_ids = sorted(set(timeline_ids), key=lambda id: extract_timestamp(id), reverse=True)
final_ids = all_ids[:50]  # primeiros 50 para a primeira página

# Batch fetch do conteúdo dos tweets selecionados
tweets = MGET [f"tweet:{id}" for id in final_ids]

O merge no application layer é barato: os tweet IDs do Twitter (Snowflake) carregam o timestamp nos bits mais significativos, então ordenar por ID é equivalente a ordenar por tempo — sem necessidade de buscar o conteúdo dos tweets só para fazer o merge.

O serviço de fan-out

# Quando um tweet é criado:
# 1. Persistir o tweet no store de tweets (fonte de verdade)
tweet_id = tweets_db.insert(user_id, content, created_at)
# Cachear o conteúdo do tweet no Redis
SETEX tweet:{tweet_id} 86400 {serialized_tweet_content}

# 2. Publicar evento no fan-out queue (Kafka/SQS)
fan_out_queue.publish({
    "tweet_id": tweet_id,
    "author_id": user_id,
    "follower_count": user.follower_count,
    "created_at": created_at
})

# 3. Fan-out service consome o evento:
def process_fan_out(event):
    if event.follower_count > CELEBRITY_THRESHOLD:
        # Celebridade: não fazer fan-out
        # O tweet aparecerá via fan-out on read no momento de leitura do timeline
        mark_as_celebrity_tweet(event.tweet_id)
        return

    # Usuário regular: buscar todos os seguidores e fazer ZADD
    followers = follow_service.get_followers(event.author_id)
    # Processar em batches para eficiência
    pipeline = redis.pipeline()
    for follower_id in followers:
        pipeline.zadd(f"timeline:{follower_id}", {event.tweet_id: event.created_at})
        # Manter o timeline capped para não crescer indefinidamente
        pipeline.zremrangebyrank(f"timeline:{follower_id}", 0, -801)  # manter top 800
    pipeline.execute()  # batch de writes Redis em uma única round-trip

Follow graph: o problema da escala

O follow graph — quem segue quem — é uma das estruturas de dados mais consultadas no sistema. Fan-out service precisa de "todos os seguidores do usuário X"; timeline read precisa de "todas as celebridades que o usuário Y segue".

# Modelo relacional simples (adequado até ~10B arestas):
CREATE TABLE follows (
    follower_id BIGINT NOT NULL,
    followee_id BIGINT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (follower_id, followee_id),
    INDEX idx_followee (followee_id, follower_id)  -- para buscar seguidores de X
);

# Consultas críticas:
# "Quem segue X?" (para fan-out)
SELECT follower_id FROM follows WHERE followee_id = :x;
# Com 100M seguidores, essa query retorna 100M linhas → não é feita em tempo real
# O fan-out service mantém essa lista pré-computada em shards do follow graph

# "A quem Y segue?" (para montar o timeline)
SELECT followee_id FROM follows WHERE follower_id = :y;
# Com Y seguindo em média 200 contas: 200 linhas — query rápida

# "Quais celebridades Y segue?" (para o merge do timeline)
SELECT followee_id FROM follows f
JOIN users u ON f.followee_id = u.id
WHERE f.follower_id = :y AND u.follower_count > CELEBRITY_THRESHOLD;
# Cacheável por usuário com TTL de 5 minutos (follows de celebridades mudam raramente)

Sharding do follow graph

Para usuários com 100M seguidores, a lista de seguidores não cabe num único shard. O Twitter usa um serviço dedicado de follow graph particionado por followee_id: todos os seguidores do usuário X estão no shard hash(X) % N. O fan-out service consulta esse shard para obter a lista de seguidores em pages (não toda de uma vez) e processa em batches paralelos.

Backfill ao fazer follow

Quando o usuário A começa a seguir o usuário B, o timeline de A deve imediatamente incluir tweets recentes de B — não só tweets futuros. Esse backfill precisa ser tratado com cuidado:

# Ao fazer follow:
def handle_follow(follower_id, followee_id):
    # 1. Persiste a aresta no follow graph
    follows_db.insert(follower_id, followee_id)

    # 2. Se followee é celebridade: sem backfill (tweets aparecerão via fan-out on read)
    if users[followee_id].follower_count > CELEBRITY_THRESHOLD:
        return

    # 3. Backfill: pegar os últimos N tweets do followee e inserir no timeline do follower
    recent_tweets = ZREVRANGE f"user_tweets:{followee_id}" 0 19  # últimos 20 tweets
    if recent_tweets:
        pipeline = redis.pipeline()
        for tweet_id in recent_tweets:
            tweet = get_tweet(tweet_id)
            pipeline.zadd(f"timeline:{follower_id}", {tweet_id: tweet.created_at})
        pipeline.execute()

# Ao fazer unfollow:
def handle_unfollow(follower_id, followee_id):
    # 1. Remove aresta do follow graph
    follows_db.delete(follower_id, followee_id)

    # 2. Remover tweets do followee do timeline do follower
    # Opção A: scan e remoção (cara para contas com muitos tweets no timeline do follower)
    followee_tweets = ZREVRANGE f"user_tweets:{followee_id}" 0 -1
    ZREM f"timeline:{follower_id}" *followee_tweets

    # Opção B: filtrar na leitura (mais simples, eventual consistency)
    # Marcar o unfollow e filtrar tweets do followee ao montar o timeline
    # Trade-off: tweets do followee ainda aparecem até o próximo refresh do timeline

Tweets deletados e edições

# Tweet deletado:
# 1. Soft delete no tweet store (deleted_at = NOW())
# 2. Não é necessário remover do timeline cache imediatamente
#    → ao fazer fetch do tweet_id, retornar null se deleted_at != null
#    → filtrar nulls ao montar o timeline para o usuário
# 3. Benefício: sem necessidade de scan do timeline de 100M seguidores ao deletar

# Tweet editado (feature introduzida em 2022):
# O tweet_id não muda (referências no timeline continuam válidas)
# O conteúdo no tweet store é atualizado com version_history
# Timeline cache armazena apenas o tweet_id → sempre busca a versão atual
# → a "edição" é transparente para o cache de timeline sem invalidação

Timeline cache: tamanho, eviction e cold start

# Tamanho do timeline cache:
# 250M usuários ativos × 800 tweet_ids × 8 bytes = 1,6 TB de memória
# → Redis Cluster com múltiplos shards (ex: 20 shards × 100 GB = 2 TB)
# O tweet_id ocupa 8 bytes (int64 Snowflake) → cache é muito eficiente em memória

# Capping da timeline:
# Manter apenas os 800 tweets mais recentes por usuário
# ZREMRANGEBYRANK timeline:{user_id} 0 -801  # remove o mais antigo quando > 800
# 800 tweets é suficiente para scrolling normal; leituras mais antigas vão ao banco

# Cold start (usuário que não acessa há muitos dias):
# O Redis pode ter evicted o timeline do usuário por pressão de memória
# → ao detectar cache miss de timeline, reconstruir do banco:
def rebuild_timeline(user_id):
    followees = follows_db.get_followees(user_id)
    # Query no banco de tweets para montar o timeline inicial
    recent_tweets = tweets_db.query("""
        SELECT tweet_id, created_at FROM tweets
        WHERE user_id = ANY(:followees)
        ORDER BY created_at DESC LIMIT 800
    """, followees=followees)
    # Populäre o cache Redis
    pipeline = redis.pipeline()
    for tweet in recent_tweets:
        pipeline.zadd(f"timeline:{user_id}", {tweet.tweet_id: tweet.created_at})
    pipeline.execute()
# Cold start tem latência alta (query no banco) mas acontece raramente
# Usuários inativos por > 30 dias: timeline reconstruído sob demanda

Do cronológico ao rankeado

O Twitter original servia tweets em ordem cronológica reversa pura. Em 2016, introduziram o "algorithmic timeline" — tweets rankeados por relevância (engajamento, relação com o usuário, diversidade de conteúdo) em vez de ordem cronológica.

Isso muda o problema de design de forma significativa:

# Para timeline rankeada:
# O fan-out service não pode pré-computar o score por usuário de forma eficiente
# Approach do Twitter (2016+): candidatos + ranking em duas etapas

# Etapa 1 — Geração de candidatos (retrieval):
# Buscar os últimos 800 tweets do feed pré-computado (fan-out on write/read híbrido)
# Este conjunto é a lista de candidatos

# Etapa 2 — Ranking (scoring):
# Para cada candidato, calcular um score usando ML model:
#   features: engagement_rate_of_tweet, relation_strength(user, author),
#             recency, topic_affinity, ...
# Ordenar por score descendente → timeline rankeado

# Custo adicional: 80k reads/s × 800 candidatos × ML inference ≈ enorme
# Solução: modelo de ML leve (linear model ou shallow tree) para o ranking inicial
#          + heavy model só para os top candidatos (reranking)
# Caching do scoring: scores de tweets populares são pré-computados e cacheados

Arquitetura completa

Usuário → API Gateway → Tweet Service
                              ↓
                        Tweet Store (persistência)
                        ├── Cassandra / Manhattan (tweets, particionado por user_id)
                        └── Redis (tweet content cache, tweet_id → serialized tweet)

                              ↓ evento publicado
                        Fan-out Queue (Kafka)
                              ↓
                        Fan-out Service
                        ├── Consulta Follow Graph Service → lista de seguidores
                        ├── Filtra celebridades (acima do threshold)
                        └── ZADD em Redis Cluster (timeline:{follower_id})

Timeline Read (home timeline):
  App Server:
  ├── ZREVRANGE timeline:{user_id} → tweet_ids (fan-out on write)
  ├── Para cada celeb seguida: ZREVRANGE user_tweets:{celeb_id} → tweet_ids
  ├── Merge + sort por Snowflake ID (≈ timestamp sort)
  ├── MGET tweet:{id} para os top-50 tweet_ids selecionados
  └── [opcional] Ranking ML nos candidatos antes de selecionar top-50

Follow Graph Service:
  ├── Storage: banco dedicado particionado por followee_id
  ├── Cache Redis: followees(user_id), celebrity_follows(user_id)
  └── API: get_followers(user_id), get_followees(user_id), is_celebrity(user_id)
o twitter timeline como paradigma de fan-out

O padrão híbrido de fan-out que o Twitter desenvolveu para resolver o celebrity problem aparece em variações em praticamente todo sistema de feed: Facebook News Feed, Instagram, LinkedIn, YouTube subscriptions, TikTok. O princípio é invariante: fan-out on write para fontes com poucos destinatários (custo distribuído em muitas escritas pequenas), fan-out on read para fontes com muitos destinatários (custo concentrado em leituras que podem ser cacheadas). O threshold entre os dois modos e o mecanismo de merge são os pontos de ajuste. O Twitter fez esse threshold em torno de 1M seguidores; outros sistemas usam thresholds diferentes baseados na capacidade de seu fan-out service.

Decisões de engenharia

Fan-out síncrono vs assíncrono

Fan-out síncrono no momento do tweet significa que o tweet só é considerado "publicado" quando todos os seguidores têm o tweet em seu feed. Para um usuário com 1M seguidores, isso adiciona segundos de latência ao POST do tweet. Inaceitável. Fan-out assíncrono via queue: o tweet é persistido imediatamente (resposta rápida ao autor), e a distribuição acontece em background.

Trade-off: com fan-out assíncrono, há uma janela onde o tweet existe mas ainda não apareceu em todos os feeds — eventual consistency. Para a maioria dos casos de uso de social feed, isso é aceitável: o usuário que postou vê o tweet imediatamente no seu próprio perfil; os seguidores veem em segundos.

Armazenar tweet_id vs conteúdo no timeline cache

O timeline cache (Redis Sorted Set) armazena apenas tweet_ids — referências, não conteúdo. O conteúdo fica no tweet store. Isso mantém o cache extremamente eficiente em memória (8 bytes por entrada vs ~500 bytes por tweet). O custo é um segundo round-trip para buscar o conteúdo após obter os IDs do timeline.

Regra prática: armazenar referências no timeline, conteúdo no tweet store com cache próprio. O tweet pode ser deletado, editado, ou ter metadados atualizados (likes, retweets) sem precisar atualizar o timeline de milhões de usuários.

Threshold de celebridade: fixo vs dinâmico

Um threshold fixo (ex: 1M seguidores) é simples mas pode ser mal calibrado: um influencer com 800k seguidores ainda causa problemas se postar muito frequentemente. Um threshold dinâmico baseado em (seguidores × frequência de posts) é mais preciso mas mais complexo de calcular e manter.

Regra prática: começar com threshold fixo e monitorar o fan-out service para outliers — contas que causam spikes mesmo abaixo do threshold. Ajustar o threshold ou adicionar regras adicionais (ex: "qualquer conta com > X follows por segundo na última hora é tratada como celebridade") baseado em dados de produção.

Unfollow: remoção imediata vs filtro eventual

Remover tweets de uma conta do timeline imediatamente após unfollow exige um scan de todo o timeline do usuário para encontrar e remover tweets daquela conta — O(n) onde n é o tamanho do timeline. Para usuários com 800 tweets no timeline e que seguem contas prolíficas, isso pode ser centenas de remoções. Filtrar na leitura é O(1) mas significa que tweets da conta recém-desfollowada ainda aparecem até o próximo refresh.

Regra prática: filtro eventual (marcar o unfollow e filtrar na leitura) é a solução correta para a maioria dos casos — o usuário raramente percebe que tweets de uma conta recém-unfollowada aparecem por 1-2 refreshes. Remoção imediata faz sentido apenas para cases de abuse (bloquear um usuário deve remover imediatamente).

Perguntas de entrevista

Por que fan-out on write não funciona para celebridades e o que exatamente colapsa?

O colapso é de throughput de writes no Redis. Considere Katy Perry com 100M seguidores postando um tweet:

O fan-out service precisa executar 100M operações ZADD no Redis — uma por seguidor. Redis single-threaded processa ~100k ops/s. O tempo necessário: 100M ÷ 100k = 1.000 segundos (~17 minutos). Nesse período, o fan-out service está completamente ocupado com o tweet de uma única celebridade, atrasando o fan-out de todos os outros tweets no sistema.

Mesmo com Redis Cluster de 20 shards (2M ops/s), seriam 50 segundos por tweet de celebridade. Se várias celebridades postam durante um evento ao vivo (SuperBowl, Oscar, notícia de última hora), o fan-out queue acumula backlog que pode levar horas para ser processado.

O segundo ponto de colapso é o follow graph: buscar 100M seguidores de uma celebridade para iniciar o fan-out requer ler uma lista enorme do banco ou cache, o que tem latência e custo de memória significativos.

A conclusão é que fan-out on write tem custo proporcional ao número de seguidores — e com distribuição de poder (alguns usuários têm 10.000x mais seguidores que a média), o sistema é dominado pelo tail do write cost, não pela média.

Como o merge de tweets de celebridades com o feed pré-computado funciona em prática?

O merge acontece no application layer no momento de cada leitura de timeline. O processo em detalhe:

1. Buscar tweet_ids do feed pré-computado: ZREVRANGE timeline:{user_id} 0 199 → 200 tweet_ids de contas regulares seguidas. Esses estão ordenados por timestamp (score do Sorted Set).

2. Buscar tweet_ids de celebridades seguidas: GET celebrity_follows:{user_id} → lista de IDs de celebridades que o usuário segue (cacheada, atualizada quando o usuário faz follow/unfollow de celebridade). Para cada celebridade: ZREVRANGE user_tweets:{celeb_id} 0 19 → 20 tweets mais recentes de cada. Com 5 celebridades seguidas: 100 tweet_ids adicionais.

3. Merge e deduplicação: combinar as duas listas (300 tweet_ids total), remover duplicatas (possível se uma conta transitou entre regular e celebridade), ordenar por timestamp. O Snowflake ID do Twitter carrega o timestamp nos primeiros 41 bits — sort por ID = sort por tempo, sem precisar buscar o conteúdo dos tweets.

4. Selecionar os top 50 para a primeira página e fazer MGET para buscar o conteúdo.

O custo total de leitura: ~3-5ms no Redis para as 3 operações, ~1ms para MGET dos 50 tweets selecionados. O merge/sort de 300 elementos em memória é sub-milissegundo.

Como o sistema mantém o timeline consistente quando um usuário segue ou deixa de seguir alguém?

Follow de conta regular: (1) persistir a aresta no follow graph, (2) backfill: buscar os últimos 20 tweets da conta seguida e inserir no timeline cache do usuário via ZADD. O usuário vê os tweets recentes da conta seguida imediatamente no próximo carregamento do timeline. Tweets mais antigos só aparecem se o usuário scrollar até onde o banco é consultado (além dos 800 no cache).

Follow de celebridade: sem backfill no fan-out on write (a celebridade está acima do threshold). Os tweets da celebridade aparecem via fan-out on read — no próximo carregamento do timeline, o application layer inclui tweets recentes da celebridade no merge. Sem overhead de backfill.

Unfollow de conta regular: duas opções. Remoção imediata: scan do timeline cache do usuário, remover todos os tweet_ids da conta desfollowada (pode ser centenas de ZREM). Filtro eventual: marcar o unfollow no follow graph, e no próximo carregamento do timeline, filtrar tweets da conta desfollowada antes de retornar. O Twitter usa filtro eventual — os tweets do desfollowado podem aparecer por 1-2 refreshes, mas o overhead de remoção imediata não vale a pena.

Unfollow de celebridade: remover a celebridade da lista celebrity_follows:{user_id} no Redis. No próximo carregamento, o merge não inclui mais tweets dessa celebridade. Imediato e barato.

Como você adicionaria suporte a listas (o usuário cria uma lista de contas e vê o feed só delas)?

Listas são um caso especial de timeline onde o conjunto de fontes é definido pelo usuário, não pelo grafo de follows. A estrutura de dados e o mecanismo de fan-out são os mesmos — o que muda é o conjunto de contas que contribuem para o feed.

Abordagem 1 — Feed pré-computado por lista (fan-out on write): cada lista tem seu próprio sorted set list_timeline:{list_id}. Quando um membro da lista posta, o fan-out service verifica em quais listas esse usuário está e adiciona o tweet ao sorted set de cada lista. Funciona bem para listas pequenas (< 100 membros). Para listas de "all verified accounts" com 500k membros, o problema de celebridade se replica.

Abordagem 2 — Fan-out on read por lista: ao carregar o feed de uma lista, consultar tweets recentes de cada membro da lista e fazer o merge. Adequado quando o usuário acessa a lista raramente (o custo de fan-out on read é pago só quando a lista é carregada, não a cada tweet de cada membro).

Abordagem híbrida: se a lista tem menos de X membros, fan-out on write. Acima de X, fan-out on read. O X para listas pode ser muito menor do que para o home timeline — listas de contas são menos frequentemente lidas.

O modelo de dados adicional necessário: tabela list_members (list_id, user_id) e sorted set list_timeline:{list_id} no Redis. O rest do mecanismo reusa a infra existente.

O que muda no design se o requisito for um feed completamente rankeado (sem cronológico)?

O feed rankeado muda o problema de "qual é o tweet mais recente?" para "qual é o tweet mais relevante para este usuário neste momento?" — o que tem implicações profundas na arquitetura.

O que não muda: o mecanismo de candidatos. O fan-out on write + merge com tweets de celebridades continua igual — ele gera um conjunto de candidatos (os tweets elegíveis para aparecer no feed deste usuário). Esse conjunto de candidatos é o input do ranker.

O que muda — ranking em duas etapas:

Etapa 1 (retrieval/light ranking): dos 800 candidatos no cache, filtrar spam, tweets já vistos, contas bloqueadas. Aplicar um modelo leve (logistic regression ou shallow tree) para pontuação inicial. Selecionar os top 200 candidatos para o heavy ranker.

Etapa 2 (heavy ranking): modelo de ML mais complexo (deep learning) que considera: taxa de engajamento do tweet nas primeiras horas, força da relação entre o usuário e o autor (frequência de interação), afinidade de tópico do usuário, diversidade do feed (evitar mostrar 10 tweets do mesmo autor). Output: top 50 tweets com score de relevância.

O custo adicional: inferência de ML para 80k reads/s × 200 candidatos é significativo. Soluções: cache de scores de tweets populares (o mesmo tweet aparece no feed de milhões de usuários — o score base pode ser pré-computado), feature store para features do usuário (relações, tópicos) que são computadas em batch e não em tempo real, e modelos leves que podem rodar em sub-milissegundo.

O Twitter (e Meta) publicaram extensamente sobre seus sistemas de ranking — é uma área ativa de pesquisa e engenharia de sistemas.

Exercícios práticos

Exercício 1 — Implementar timeline com Redis Sorted Set

Implemente um servidor simplificado de timeline com Redis. Endpoints: POST /tweets (cria tweet e faz fan-out para seguidores), GET /timeline/{user_id} (retorna os 20 tweets mais recentes do feed). Use Redis Sorted Set com timestamp como score. Implemente o capping do timeline em 100 tweets (ZREMRANGEBYRANK após cada inserção). Meça: latência de POST com 100 seguidores, 1.000 seguidores, e 10.000 seguidores — onde o fan-out síncrono começa a impactar a latência do POST?

Critério: o GET /timeline retorna tweets em ordem cronológica correta. O fan-out com 10.000 seguidores causa latência de POST visivelmente maior — e você pode articular por que o fan-out assíncrono é a solução correta para essa escala.

Exercício 2 — Simular o celebrity problem com threshold híbrido

Estendendo o exercício 1: adicione suporte a usuários "celebridade" (acima de 1.000 seguidores no seu teste). Para celebridades, não faça fan-out on write — armazene seus tweets num sorted set separado user_tweets:{user_id}. No GET /timeline, implemente o merge: sorted set do feed pré-computado + tweets recentes de celebridades seguidas pelo usuário. Compare a latência de POST de um usuário regular vs celebridade. Compare a latência de GET com e sem celebridades no feed.

Critério: POST de celebridade tem latência constante independente do número de seguidores. GET do timeline com 3 celebridades seguidas retorna tweets corretamente mergeados em ordem cronológica. O merge acontece sem buscar o conteúdo dos tweets — só tweet_ids.

Exercício 3 — Modelar o follow graph e backfill

Implemente follow/unfollow com backfill. Ao fazer follow de uma conta regular: buscar os últimos 10 tweets da conta seguida e inserir no timeline do seguidor. Ao fazer unfollow: implementar filtro eventual (marcar o unfollow, não remover imediatamente do cache). No GET /timeline, filtrar tweets de contas desfollowadas. Teste: siga uma conta com 5 tweets existentes e verifique que os tweets aparecem imediatamente. Deixe de seguir e verifique que os tweets desaparecem no próximo GET.

Critério: backfill funciona corretamente e é O(tweets_da_conta), não O(tamanho_do_timeline). Filtro eventual funciona sem remover entradas do Sorted Set. O GET /timeline não retorna tweets de contas desfollowadas após a primeira leitura pós-unfollow.

Exercício 4 — Analisar o custo de memória do timeline cache

Com Redis rodando localmente, simule 10.000 usuários cada um com um timeline de 800 tweet_ids (use ZADD com valores aleatórios). Meça: uso de memória total do Redis, uso de memória por timeline, e o impacto de reduzir o cap de 800 para 200, 100, e 50 tweets. Estime, extrapolando, quanto de memória seria necessário para 250M usuários ativos com timelines de 800 tweets. Esse número suporta a decisão de armazenar apenas tweet_ids (não conteúdo) no sorted set?

Critério: você mediu empiricamente o uso de memória por timeline e calculou a extrapolação para 250M usuários. A comparação entre armazenar tweet_ids (8 bytes cada) vs tweets completos (~500 bytes cada) demonstra quantitativamente por que a abordagem de referência é necessária nessa escala.

Exercício 5 — Design de feed rankeado: candidatos + scoring

Implemente uma versão simplificada de feed rankeado. A partir dos candidatos do timeline cache (tweet_ids + timestamps), adicione um score composto: 70% recência (quanto mais recente, maior o score) + 30% engajamento simulado (número aleatório representando likes + retweets). O endpoint GET /timeline/{user_id}?mode=ranked retorna os tweets ordenados pelo score composto em vez do timestamp. Compare lado a lado: cronológico vs rankeado com 20 candidatos — o rankeado produz uma ordem diferente? Em que porcentagem dos casos o tweet mais rankeado não é o mais recente?

Critério: o feed rankeado é implementado em duas etapas explícitas (retrieval de candidatos + scoring). A lógica de scoring é separada da lógica de retrieval — permitindo mudar o modelo de score sem alterar como os candidatos são buscados. Você consegue articular por que o custo de scoring cresce com o número de candidatos e não com o número de usuários.

Referências

  1. article Twitter Engineering — Timelines at Scale blog.twitter.com · a apresentação original do Twitter sobre o sistema de timeline: fan-out híbrido, Redis Sorted Set, e o celebrity problem
  2. video Twitter — QCon 2014: Timelines at Scale infoq.com · talk técnico do Twitter Engineering detalhando a arquitetura do fan-out service e as decisões que levaram ao modelo híbrido
  3. article Meta Engineering — Scaling the Facebook News Feed engineering.fb.com · como o Facebook implementa o News Feed com ranking — similar ao Twitter mas com ênfase em ML desde mais cedo
  4. article Instagram Engineering — Rewriting the Feed instagram-engineering.com · como o Instagram migrou de feed cronológico para rankeado mantendo a infra de fan-out existente
  5. book Martin Kleppmann — DDIA Chapter 11: Stream Processing O'Reilly · 2017 · stream processing e event sourcing — a base teórica do fan-out assíncrono via Kafka/queue
  6. docs Redis — Sorted Sets redis.io/docs/data-types/sorted-sets · a estrutura de dados usada para o timeline cache — operações, complexidade, e casos de uso
  7. article High Scalability — How Twitter Stores 250 Million Tweets a Day highscalability.com · análise do sistema de storage de tweets do Twitter — Manhattan (sistema distribuído proprietário) e as decisões de escalabilidade
  8. article Twitter — Open-sourcing Snowflake blog.twitter.com/engineering · o anúncio do Snowflake ID — como IDs com timestamp embutido resolvem o problema de sort sem fetch de conteúdo
  9. article Netflix Tech Blog — System Architectures for Personalization and Recommendation netflixtechblog.com · o padrão de candidatos + ranking em duas etapas que aparece em feed rankeado — candidato retrieval leve e heavy ranker
  10. article LinkedIn Engineering — The Architecture of LinkedIn's Real-Time Activity Data Pipeline engineering.linkedin.com · fan-out no contexto de feeds profissionais — semelhanças e diferenças em relação ao Twitter
  11. book Alex Xu — System Design Interview vol. 1, Chapter 11: Design a News Feed System bytebytego.com · tratamento completo do problema de news feed com o modelo híbrido de fan-out, diagrama de arquitetura e análise de trade-offs
  12. article Twitter — Handling Five Billion Sessions a Day in Real Time blog.twitter.com · os números reais de sessões e throughput do Twitter em escala — contexto para as decisões arquiteturais do timeline