Um guia completo de system design para construir uma plataforma social como o Twitter, lidando com bilhões de tweets, timelines em tempo real e assimetria massiva de leitura/escrita.
Curated resources to complement your reading
Software Architect
Ajudo founders a parar de lutar com decisões de arquitetura e começar a entregar. Após 10+ anos construindo produtos em três continentes, aprendi que o melhor código é invisível—ele funciona, escala e converte. Pós-graduado em arquitetura de software e soluções. Conecto profundidade técnica com resultados de negócio para entregar produtos que as pessoas realmente usam. Também mentoro desenvolvedores e criadores em programas ao vivo, podcasts e iniciativas de comunidade focadas em tecnologia inclusiva.
47-point checklist to catch bugs, security risks, and performance issues before launch.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
Modular packages for founders and engineering leads. Every engagement includes diagnosis, documentation, and direct access.
2 advisory slots for Q2

São 3 da manhã de um domingo. Beyoncé acabou de lançar um álbum surpresa e tuitou sobre isso. Nos próximos 60 segundos, 300.000 pessoas retuitam o anúncio. Cada um desses retweets precisa aparecer nas timelines de todos os seguidores -- algumas contas têm 50 milhões de seguidores. Isso são potencialmente 15 trilhões de inserções em timelines disparadas por um único tweet.
Bem-vindo ao desafio mais fascinante de sistemas distribuídos nas redes sociais: projetar o Twitter em escala.
Isso não é mais um overview superficial que te diz "use um load balancer." Isso é uma exploração aprofundada e testada em batalha de cada decisão arquitetural que torna uma plataforma como o X.com possível. Vamos cobrir os trade-offs exatos que o time de engenharia do Twitter enfrentou, as soluções que escolheram e o porquê de cada uma.
Se você está se preparando para uma entrevista de system design em uma grande empresa de tecnologia, arquitetando sua própria plataforma social, ou simplesmente curioso sobre como 500 milhões de tweets por dia chegam a bilhões de timelines em milissegundos -- este guia tem tudo que você precisa.
Vamos construir o Twitter do zero.
Todos os diagramas utlizados abaixo estão disponíveis no excalidraw: https://link.excalidraw.com/l/7XRBb57RGJp/39OlGPsnkZR
Antes de escrever uma única linha de código ou desenhar qualquer diagrama de arquitetura, precisamos entender profundamente o que estamos construindo. Em uma entrevista de system design, gastar de 5 a 8 minutos em requisitos não é tempo desperdiçado -- é a base que impede você de construir o sistema errado.
Funcionalidades Essenciais (Must Have):
Funcionalidades Estendidas (Nice to Have):
| Requisito | Meta | Justificativa |
|---|---|---|
| Disponibilidade | 99,99% (52 min de downtime/ano) | Plataforma social; usuários esperam disponibilidade constante |
| Latência da Timeline | < 200ms p99 | Usuários rolam rapidamente; deve parecer instantâneo |
| Latência de Postagem | < 500ms p99 | Usuários esperam publicação quase instantânea |
| Modelo de Consistência | Consistência eventual (timeline), Forte (follows, DMs) | Timeline pode ter leve atraso; follows devem ser precisos |
| Durabilidade | Nenhum tweet perdido após confirmação | Cada tweet é um registro permanente |
| Proporção Leitura:Escrita | 1000:1 | Amplificação massiva de leitura devido às timelines |
| Tolerância a Partições | Obrigatória | Sistema global deve tolerar partições de rede |
Esses cálculos são críticos em entrevistas. Eles demonstram maturidade de engenharia e ajudam a direcionar decisões arquiteturais.
Tweets por dia: 500M
Tamanho médio do tweet (texto + metadados): ~300 bytes
Armazenamento diário de tweets: 500M x 300B = 150 GB/dia
Armazenamento anual de tweets: 150 GB x 365 = ~55 TB/ano
Armazenamento em 5 anos: ~275 TB (apenas texto)
Mídia (imagens/vídeo):
30% dos tweets têm mídia
Tamanho médio de mídia: 500 KB (imagens), 5 MB (vídeo)
Assumindo 80% imagens, 20% vídeo:
Mídia diária: 500M x 0.3 x (0.8 x 500KB + 0.2 x 5MB)
= 150M x (400KB + 1MB)
= 150M x 1.4MB = 210 TB/dia
Mídia anual: 210 TB x 365 = ~76 PB/ano
Largura de banda de leitura (timeline):
250B leituras de timeline/dia x 10 tweets por página x 300 bytes
= 750 TB/dia = ~8,7 GB/seg
Largura de banda de escrita (novos tweets):
6.000 tweets/seg x 300 bytes = 1,8 MB/seg (trivial)
Largura de banda de mídia é dominante:
Assumindo que 50% das visualizações de timeline incluem carregamento de mídia
= ~4 TB/seg no pico (servido primariamente via CDN)
Criação de tweets: 6.000/seg (média), 12.000/seg (pico)
Leituras de timeline: 250B/86400 ~ 3M/seg (média), 6M/seg (pico)
Consultas de busca: ~100K/seg
Curtida/Retweet: ~50K/seg
Seguir/Deixar de seguir: ~5K/seg
Insight Principal: A proporção leitura-para-escrita é de aproximadamente 500:1 para operações de timeline. Essa assimetria extrema é o fator mais importante que direciona toda a nossa arquitetura.
Por que Microserviços? O Twitter começou como um monolito em Ruby on Rails. Em 2012, eles migraram para uma arquitetura de microserviços porque:
Por que GraphQL Federation? Em vez de cada cliente conversar com dezenas de serviços, uma camada de federação GraphQL oferece:
// ===== Tweet Service API =====
interface CreateTweetRequest {
text: string; // max 280 chars (free) ou 25,000 (premium)
media_ids?: string[]; // referências de mídia pré-carregadas
reply_to?: string; // tweet ID se for uma resposta
quote_tweet_id?: string; // tweet ID se for um quote tweet
poll?: PollOptions; // enquete opcional
conversation_settings?: 'everyone' | 'following' | 'mentioned';
}
interface Tweet {
id: string; // Snowflake ID
user_id: string;
text: string;
created_at: string; // ISO 8601
media: MediaAttachment[];
metrics: {
retweet_count: number;
like_count: number;
reply_count: number;
view_count: number;
};
reply_to?: string;
conversation_id: string;
language: string;
source: string; // "Twitter for iPhone", etc.
}
// POST /api/v2/tweets
// GET /api/v2/tweets/:id
// DELETE /api/v2/tweets/:id
// ===== Timeline Service API =====
interface TimelineRequest {
cursor?: string; // cursor de paginação (string opaca)
count?: number; // padrão 20, máximo 200
algorithm?: 'reverse_chronological' | 'ranked';
}
interface TimelineResponse {
tweets: Tweet[];
cursor_top: string; // para tweets mais recentes (pull to refresh)
cursor_bottom: string; // para tweets mais antigos (scroll infinito)
}
// GET /api/v2/timeline/home?cursor=xxx&count=20
// GET /api/v2/timeline/user/:userId?cursor=xxx
// ===== Social Graph API =====
interface FollowRequest {
target_user_id: string;
}
// POST /api/v2/users/:id/follow
// DELETE /api/v2/users/:id/follow
// GET /api/v2/users/:id/followers?cursor=xxx
// GET /api/v2/users/:id/following?cursor=xxx
// ===== Search API =====
interface SearchRequest {
query: string;
type: 'tweets' | 'users' | 'hashtags';
since?: string; // filtro de data
until?: string;
from?: string; // usuário específico
lang?: string;
cursor?: string;
count?: number;
}
// GET /api/v2/search?q=hello&type=tweets&cursor=xxx
// ===== Engagement API =====
// POST /api/v2/tweets/:id/like
// DELETE /api/v2/tweets/:id/like
// POST /api/v2/tweets/:id/retweet
// DELETE /api/v2/tweets/:id/retweet
interface RateLimitConfig {
// Limites por usuário (janela deslizante)
tweet_create: { requests: 300, window: '3h' };
timeline_read: { requests: 1500, window: '15m' };
search: { requests: 450, window: '15m' };
follow: { requests: 400, window: '24h' };
like: { requests: 1000, window: '24h' };
dm_send: { requests: 500, window: '24h' };
// Limites por aplicação (para consumidores da API)
app_tweet_read: { requests: 300000, window: '15m' };
app_tweet_create: { requests: 200, window: '15m' };
}
// Conexão WebSocket para funcionalidades em tempo real
// wss://stream.x.com/v2/stream
interface StreamEvent {
type: 'new_tweet' | 'like' | 'retweet' | 'reply' |
'follow' | 'dm' | 'notification' | 'typing';
data: Record<string, unknown>;
timestamp: string;
}
// O cliente se inscreve em streams específicos:
// - atualizações da timeline do usuário
// - stream de notificações
// - atualizações de conversas de DM
// - indicadores de digitação
-- Tabela de usuários (PostgreSQL, sharded por user_id)
CREATE TABLE users (
id BIGINT PRIMARY KEY, -- Snowflake ID
username VARCHAR(15) UNIQUE NOT NULL,
display_name VARCHAR(50),
bio TEXT,
profile_image VARCHAR(255),
banner_image VARCHAR(255),
verified BOOLEAN DEFAULT FALSE,
is_premium BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
followers_count INT DEFAULT 0,
following_count INT DEFAULT 0,
tweet_count INT DEFAULT 0,
location VARCHAR(100),
website VARCHAR(200)
);
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_created_at ON users(created_at);
-- Tabela de tweets (Cassandra / Manhattan, particionada por user_id)
CREATE TABLE tweets (
id BIGINT PRIMARY KEY, -- Snowflake ID (contém timestamp)
user_id BIGINT NOT NULL,
content TEXT,
reply_to_tweet_id BIGINT,
conversation_id BIGINT,
quote_tweet_id BIGINT,
language VARCHAR(5),
source VARCHAR(50),
media_keys TEXT[], -- array de IDs de mídia
created_at TIMESTAMPTZ NOT NULL,
retweet_count INT DEFAULT 0,
like_count INT DEFAULT 0,
reply_count INT DEFAULT 0,
view_count BIGINT DEFAULT 0,
is_sensitive BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_tweets_user_id ON tweets(user_id, created_at DESC);
CREATE INDEX idx_tweets_conversation ON tweets(conversation_id, created_at);
-- Relacionamentos de follow (Grafo Social - FlockDB / Cassandra)
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
following_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (follower_id, following_id)
);
-- Índice reverso para "quem me segue"
CREATE INDEX idx_follows_following ON follows(following_id, follower_id);
-- Curtidas (Cassandra, alto throughput de escrita)
CREATE TABLE likes (
user_id BIGINT NOT NULL,
tweet_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, tweet_id)
);
CREATE INDEX idx_likes_tweet ON likes(tweet_id, created_at DESC);
O Twitter inventou o sistema de Snowflake IDs, que agora é usado em toda a indústria. Entender isso é essencial.
/**
* Twitter Snowflake ID Generator
*
* Estrutura do ID de 64 bits:
* [1 bit não usado][41 bits timestamp][5 bits datacenter][5 bits worker][12 bits sequência]
*
* - Timestamp: milissegundos desde uma época customizada (Twitter: 4 Nov, 2010)
* - Suporta ~69 anos de timestamps
* - 4096 IDs únicos por milissegundo por worker
* - Ordenável por tempo: IDs mais novos são sempre maiores
*/
class SnowflakeGenerator {
private static EPOCH = 1288834974657n; // Época do Twitter: 4 Nov, 2010
private static DATACENTER_BITS = 5n;
private static WORKER_BITS = 5n;
private static SEQUENCE_BITS = 12n;
private static MAX_SEQUENCE = (1n << this.SEQUENCE_BITS) - 1n; // 4095
private static WORKER_SHIFT = this.SEQUENCE_BITS;
private static DATACENTER_SHIFT = this.SEQUENCE_BITS + this.WORKER_BITS;
private static TIMESTAMP_SHIFT = this.SEQUENCE_BITS + this.WORKER_BITS + this.DATACENTER_BITS;
private sequence = 0n;
private lastTimestamp = -1n;
constructor(
private datacenterId: bigint,
private workerId: bigint
) {}
generate(): bigint {
let timestamp = BigInt(Date.now());
if (timestamp === this.lastTimestamp) {
this.sequence = (this.sequence + 1n) & SnowflakeGenerator.MAX_SEQUENCE;
if (this.sequence === 0n) {
// Espera pelo próximo milissegundo
while (timestamp <= this.lastTimestamp) {
timestamp = BigInt(Date.now());
}
}
} else {
this.sequence = 0n;
}
this.lastTimestamp = timestamp;
return (
((timestamp - SnowflakeGenerator.EPOCH) << SnowflakeGenerator.TIMESTAMP_SHIFT) |
(this.datacenterId << SnowflakeGenerator.DATACENTER_SHIFT) |
(this.workerId << SnowflakeGenerator.WORKER_SHIFT) |
this.sequence
);
}
// Extrai timestamp de um Snowflake ID
static extractTimestamp(id: bigint): Date {
const timestamp = (id >> this.TIMESTAMP_SHIFT) + this.EPOCH;
return new Date(Number(timestamp));
}
}
// Uso
const generator = new SnowflakeGenerator(1n, 1n);
const tweetId = generator.generate();
// Resultado: 1784567890123456789n (ordenável por tempo, globalmente único)
Por que Snowflake IDs são importantes:
Esta é a seção mais importante de todo este artigo. A geração da timeline é o que torna o Twitter arquiteturalmente fascinante e é o que os entrevistadores mais querem saber.
A pergunta é enganosamente simples: Como você mostra a um usuário os tweets mais recentes das pessoas que ele segue?
A abordagem mais simples: quando um usuário abre sua timeline, consulte todos os usuários que ele segue e combine seus tweets recentes.
-- Quando o usuário 123 abre sua timeline:
SELECT t.* FROM tweets t
JOIN follows f ON f.following_id = t.user_id
WHERE f.follower_id = 123
ORDER BY t.created_at DESC
LIMIT 20;
Problemas:
Em vez de computar timelines no momento da leitura, pré-computamos no momento da escrita. Quando um usuário posta um tweet, entregamos imediatamente para os caches de timeline de todos os seus seguidores.
Como funciona em detalhes:
class FanOutService {
constructor(
private socialGraph: SocialGraphClient,
private timelineCache: RedisCluster,
private tweetStore: TweetStore,
private kafka: KafkaProducer
) {}
async handleNewTweet(event: TweetCreatedEvent): Promise<void> {
const { tweetId, authorId } = event;
// Obtém todos os seguidores do autor
const followers = await this.socialGraph.getFollowers(authorId);
// Fan-out para o cache de timeline de cada seguidor
const pipeline = this.timelineCache.pipeline();
for (const followerId of followers) {
// Insere o tweet ID no início da lista de timeline do seguidor
pipeline.lpush(`timeline:${followerId}`, tweetId);
// Trim para manter apenas os 800 tweets mais recentes
pipeline.ltrim(`timeline:${followerId}`, 0, 799);
}
await pipeline.exec();
// Registra métricas do fan-out
this.metrics.recordFanOut(authorId, followers.length);
}
async getTimeline(userId: string, cursor: number = 0, count: number = 20): Promise<Tweet[]> {
// Leitura agora é O(1) - apenas lê da lista Redis
const tweetIds = await this.timelineCache.lrange(
`timeline:${userId}`,
cursor,
cursor + count - 1
);
// Hidrata os tweet IDs em objetos completos de tweet (leitura em lote)
const tweets = await this.tweetStore.multiGet(tweetIds);
return tweets;
}
}
Trade-offs do Fan-Out on Write:
| Aspecto | Benefício | Custo |
|---|---|---|
| Latência de leitura | < 5ms (leitura de lista Redis) | -- |
| Amplificação de escrita | -- | 1 tweet -> N inserções de timeline |
| Armazenamento | -- | IDs de tweet duplicados em timelines |
| Consistência | Entrega quase em tempo real | Leve atraso para fan-outs grandes |
| Complexidade | Caminho de leitura simples | Caminho de escrita complexo |
Fan-out on write funciona muito bem para usuários normais. Mas o que acontece quando Elon Musk (180M+ seguidores) posta um tweet?
Fan-out para um usuário normal (500 seguidores):
500 escritas Redis x 0,1ms = 50ms total
Fan-out para Elon Musk (180M seguidores):
180.000.000 escritas Redis x 0,1ms = 18.000 segundos = 5 horas
Isso é chamado de Problema das Celebridades ou Problema de Hot Key, e é uma das decisões de design mais críticas do sistema.
O Twitter usa uma estratégia de fan-out híbrido que combina ambas as abordagens:
O Algoritmo Híbrido:
class HybridTimelineService {
private readonly CELEBRITY_THRESHOLD = 100_000; // seguidores
async getTimeline(userId: string, count: number = 20): Promise<Tweet[]> {
// Passo 1: Obter timeline pré-computada (resultados do fan-out on write)
const cachedTweetIds = await this.timelineCache.lrange(
`timeline:${userId}`, 0, count * 2 // busca extra para o merge
);
// Passo 2: Obter tweets de celebridades (fan-out on read)
const celebritiesFollowed = await this.getCelebritiesFollowedBy(userId);
const celebrityTweets: Tweet[] = [];
for (const celeb of celebritiesFollowed) {
const recentTweets = await this.tweetStore.getRecentTweets(
celeb.id,
{ limit: 5, since: this.getTimelineFreshness() }
);
celebrityTweets.push(...recentTweets);
}
// Passo 3: Hidratar os tweet IDs cacheados
const cachedTweets = await this.tweetStore.multiGet(cachedTweetIds);
// Passo 4: Combinar ambas as fontes
const merged = this.mergeSorted(cachedTweets, celebrityTweets);
// Passo 5: Aplicar algoritmo de ranking (se não estiver no modo cronológico)
const ranked = await this.rankingService.rank(userId, merged);
return ranked.slice(0, count);
}
async handleNewTweet(event: TweetCreatedEvent): Promise<void> {
const followerCount = await this.socialGraph.getFollowerCount(event.authorId);
if (followerCount < this.CELEBRITY_THRESHOLD) {
// Usuário normal: fan-out on write
await this.fanOutService.fanOut(event);
} else {
// Celebridade: armazena o tweet, será buscado na leitura
await this.celebrityTweetCache.addTweet(event.authorId, event.tweetId);
// Ainda faz fan-out para um subconjunto pequeno (ex: seguidores verificados, usuários ativos)
const priorityFollowers = await this.socialGraph.getPriorityFollowers(
event.authorId,
{ limit: 10000 }
);
await this.fanOutService.fanOutToSubset(event, priorityFollowers);
}
}
private getCelebritiesFollowedBy(userId: string): Promise<User[]> {
// Lista cacheada de contas de celebridades que este usuário segue
// Atualizada quando o usuário segue/deixa de seguir uma celebridade
return this.cache.get(`celeb_following:${userId}`);
}
}
Por que a abordagem híbrida funciona:
Usuários normais (99% das contas): Fan-out on write. Seus tweets chegam às timelines de todos os seguidores instantaneamente via pré-computação.
Celebridades (top 0,1%): Seus tweets NÃO sofrem fan-out. Em vez disso, quando um seguidor lê sua timeline, o sistema busca tweets de celebridades sob demanda e combina com a timeline pré-computada.
A matemática funciona:
O Twitter moderno não mostra uma timeline puramente cronológica. Um sistema de ranking baseado em ML determina a ordem dos tweets:
interface TweetFeatures {
// Features no nível do tweet
tweet_age_seconds: number;
has_media: boolean;
has_url: boolean;
tweet_length: number;
is_reply: boolean;
is_retweet: boolean;
// Features do autor
author_follower_count: number;
author_verified: boolean;
author_tweet_frequency: number;
// Features de engajamento (tempo real)
like_count: number;
retweet_count: number;
reply_count: number;
like_velocity: number; // curtidas por minuto (sinal de trending)
// Relacionamento usuário-autor
interaction_history: number; // frequência de interação do usuário com o autor
is_mutual_follow: boolean;
last_interaction_days: number;
// Features de contexto
user_active_hours_match: boolean;
topic_interest_score: number;
}
// Cálculo simplificado do score de ranking
function calculateRankingScore(features: TweetFeatures): number {
const weights = {
recency: 0.30,
engagement: 0.25,
relationship: 0.20,
relevance: 0.15,
author_quality: 0.10
};
const recencyScore = Math.exp(-features.tweet_age_seconds / 3600);
const engagementScore = normalizeEngagement(features);
const relationshipScore = features.interaction_history / 100;
const relevanceScore = features.topic_interest_score;
const authorScore = features.author_verified ? 0.8 : 0.5;
return (
weights.recency * recencyScore +
weights.engagement * engagementScore +
weights.relationship * relationshipScore +
weights.relevance * relevanceScore +
weights.author_quality * authorScore
);
}
Quando um usuário posta um tweet, isso dispara uma cascata de operações assíncronas. O caminho de escrita é projetado para durabilidade e propagação eventual.
class TweetService {
async createTweet(userId: string, request: CreateTweetRequest): Promise<Tweet> {
// Passo 1: Validar entrada
this.validateTweetContent(request);
// Passo 2: Verificar limites de taxa
await this.rateLimiter.checkLimit(userId, 'tweet_create');
// Passo 3: Detecção de spam/abuso (síncrono, modelo ML rápido)
const spamScore = await this.spamDetector.score(userId, request.text);
if (spamScore > 0.95) {
throw new SpamDetectedError('Tweet flagged as spam');
}
// Passo 4: Gerar Snowflake ID
const tweetId = this.snowflake.generate();
// Passo 5: Processar referências de mídia
const mediaKeys = request.media_ids
? await this.mediaService.validateAndLink(request.media_ids, tweetId)
: [];
// Passo 6: Detectar idioma
const language = await this.languageDetector.detect(request.text);
// Passo 7: Extrair entidades (hashtags, menções, URLs)
const entities = this.entityExtractor.extract(request.text);
// Passo 8: Armazenar tweet (escrita síncrona no armazenamento primário)
const tweet: Tweet = {
id: tweetId,
user_id: userId,
text: request.text,
media_keys: mediaKeys,
language,
entities,
reply_to_tweet_id: request.reply_to,
conversation_id: request.reply_to
? await this.getConversationId(request.reply_to)
: tweetId,
created_at: new Date().toISOString(),
retweet_count: 0,
like_count: 0,
reply_count: 0,
view_count: 0
};
await this.tweetStore.save(tweet);
// Passo 9: Publicar evento para processamento assíncrono
await this.kafka.publish('tweet-events', {
type: 'TWEET_CREATED',
data: tweet,
timestamp: Date.now()
});
// Passo 10: Retornar imediatamente (não espera pelo fan-out)
return tweet;
}
}
// Configuração de tópicos para eventos de tweet
const kafkaTopics = {
'tweet-events': {
partitions: 128, // Alto paralelismo para fan-out
replicationFactor: 3, // Durabilidade
retentionMs: 7 * 24 * 3600 * 1000, // 7 dias
partitionKey: 'user_id', // Todos os tweets do mesmo usuário vão para a mesma partição
// Isso garante ordenação por usuário
},
'timeline-updates': {
partitions: 256, // Paralelismo ainda maior
replicationFactor: 3,
retentionMs: 24 * 3600 * 1000, // 24 horas
partitionKey: 'follower_id',
},
'notification-events': {
partitions: 64,
replicationFactor: 3,
retentionMs: 7 * 24 * 3600 * 1000,
partitionKey: 'target_user_id',
},
'search-indexing': {
partitions: 32,
replicationFactor: 3,
retentionMs: 3 * 24 * 3600 * 1000,
partitionKey: 'tweet_id',
}
};
O sistema de busca do Twitter processa aproximadamente 100.000 consultas por segundo em bilhões de tweets.
// Como os tweets são indexados para busca
interface InvertedIndex {
// Termo -> Lista de (tweet_id, posições, score)
// Exemplo: "typescript" -> [(tweet_123, [0], 0.95), (tweet_456, [3, 7], 0.82)]
term: string;
postings: Array<{
tweetId: string;
positions: number[]; // Posições da palavra no tweet
termFrequency: number; // Quantas vezes o termo aparece
fieldBoost: number; // Boost para hashtags vs texto do corpo
timestamp: number; // Para scoring de recência
}>;
}
// Função de scoring de busca
function searchScore(query: string, tweet: IndexedTweet): number {
const textRelevance = bm25Score(query, tweet.text); // BM25 text matching
const recencyBoost = recencyDecay(tweet.timestamp); // Mais recente = melhor
const engagementSignal = Math.log1p( // Sinal de engajamento
tweet.likeCount + tweet.retweetCount * 2
);
const authorAuthority = tweet.authorFollowerCount > 10000 ? 1.2 : 1.0;
return textRelevance * 0.4
+ recencyBoost * 0.3
+ engagementSignal * 0.2
+ authorAuthority * 0.1;
}
Trending não é apenas sobre volume -- é sobre velocidade. Um tópico está em trending se está crescendo significativamente mais rápido que sua linha de base.
class TrendingService {
// Contadores de janela deslizante usando Redis
private readonly WINDOW_SIZES = [
{ name: '5min', seconds: 300 },
{ name: '1hour', seconds: 3600 },
{ name: '24hour', seconds: 86400 },
];
async trackHashtag(hashtag: string, tweetId: string): Promise<void> {
const now = Math.floor(Date.now() / 1000);
for (const window of this.WINDOW_SIZES) {
const bucket = Math.floor(now / 60); // granularidade de 1 minuto
const key = `trending:${window.name}:${bucket}`;
await this.redis.zincrby(key, 1, hashtag);
await this.redis.expire(key, window.seconds);
}
}
async getTrending(location?: string): Promise<TrendingTopic[]> {
// Obter contagens atuais de 5 minutos
const currentCounts = await this.getCurrentCounts();
// Obter linha de base (média de 24 horas por janela de 5 minutos)
const baselines = await this.getBaselines();
// Calcular score de trending: quanto acima da linha de base?
const scores: TrendingTopic[] = [];
for (const [hashtag, count] of currentCounts) {
const baseline = baselines.get(hashtag) || 1;
const velocity = count / baseline;
// Só é trending se velocidade > 2x a linha de base E volume mínimo
if (velocity > 2.0 && count > 100) {
scores.push({
hashtag,
tweetCount: count,
velocity,
score: velocity * Math.log1p(count),
location: location || 'global'
});
}
}
// Ordena por score, retorna top 30
return scores
.sort((a, b) => b.score - a.score)
.slice(0, 30);
}
}
class NotificationProcessor {
async processEvent(event: EngagementEvent): Promise<void> {
const { type, actorId, targetUserId, tweetId } = event;
// Passo 1: Verificar se a notificação deve ser enviada
const prefs = await this.userPreferences.get(targetUserId);
if (!this.shouldNotify(prefs, type, actorId)) {
return;
}
// Passo 2: Agrupar notificações similares
// "João e outras 5 pessoas curtiram seu tweet" em vez de 6 notificações separadas
const coalescedKey = `${type}:${tweetId}:${this.getTimeWindow()}`;
const coalesced = await this.coalescer.addAndGet(coalescedKey, actorId);
// Passo 3: Criar registro de notificação
const notification: Notification = {
id: this.snowflake.generate(),
userId: targetUserId,
type,
actors: coalesced.actors,
tweetId,
read: false,
createdAt: new Date()
};
await this.notificationStore.save(notification);
// Passo 4: Determinar canais de entrega
const channels = this.determineChannels(prefs, type, coalesced);
// Passo 5: Entregar
if (channels.includes('websocket')) {
await this.websocketManager.send(targetUserId, {
type: 'notification',
data: notification
});
}
if (channels.includes('push') && coalesced.isFirst) {
// Só envia push na primeira ocorrência, não a cada adição agrupada
await this.pushService.send(targetUserId, {
title: this.formatTitle(notification),
body: this.formatBody(notification),
data: { tweetId, type }
});
}
}
private shouldNotify(
prefs: UserPreferences,
type: string,
actorId: string
): boolean {
// Não notificar para:
// - Usuários silenciados
// - Usuários bloqueados
// - Tipos de notificação desabilitados
// - Auto-interações
return (
!prefs.mutedUsers.includes(actorId) &&
!prefs.blockedUsers.includes(actorId) &&
prefs.enabledTypes.includes(type)
);
}
}
Mensagens diretas têm requisitos fundamentalmente diferentes dos tweets:
class DMService {
async sendMessage(
senderId: string,
conversationId: string,
content: DMContent
): Promise<DirectMessage> {
// Passo 1: Validar que o remetente faz parte da conversa
const conversation = await this.conversationStore.get(conversationId);
if (!conversation.participants.includes(senderId)) {
throw new ForbiddenError('Not a participant');
}
// Passo 2: Gerar ID de mensagem ordenado
const messageId = this.snowflake.generate();
// Passo 3: Armazenar mensagem
const message: DirectMessage = {
id: messageId,
conversationId,
senderId,
content: content.text,
mediaKeys: content.mediaIds || [],
createdAt: new Date(),
readBy: [senderId]
};
await this.messageStore.save(message);
// Passo 4: Atualizar metadados da conversa
await this.conversationStore.updateLastMessage(conversationId, message);
// Passo 5: Entrega em tempo real via WebSocket
for (const participantId of conversation.participants) {
if (participantId === senderId) continue;
const isOnline = await this.presenceService.isOnline(participantId);
if (isOnline) {
await this.websocketManager.send(participantId, {
type: 'dm',
data: message
});
} else {
// Enfileirar para entrega offline + notificação push
await this.offlineQueue.enqueue(participantId, message);
await this.pushService.send(participantId, {
title: `New message from ${await this.getDisplayName(senderId)}`,
body: this.truncate(content.text, 100)
});
}
}
return message;
}
}
interface ImageProcessingConfig {
variants: Array<{
name: string;
maxWidth: number;
maxHeight: number;
quality: number;
format: 'jpeg' | 'webp' | 'avif';
}>;
}
const TWEET_IMAGE_CONFIG: ImageProcessingConfig = {
variants: [
{ name: 'thumb', maxWidth: 150, maxHeight: 150, quality: 80, format: 'jpeg' },
{ name: 'small', maxWidth: 400, maxHeight: 400, quality: 85, format: 'webp' },
{ name: 'medium', maxWidth: 680, maxHeight: 680, quality: 85, format: 'webp' },
{ name: 'large', maxWidth: 1200, maxHeight: 1200, quality: 90, format: 'webp' },
{ name: 'orig', maxWidth: 4096, maxHeight: 4096, quality: 95, format: 'avif' },
]
};
class ImageProcessor {
async process(imageKey: string, config: ImageProcessingConfig): Promise<ProcessedImage[]> {
const original = await this.storage.get(imageKey);
const results: ProcessedImage[] = [];
for (const variant of config.variants) {
const processed = await sharp(original)
.resize(variant.maxWidth, variant.maxHeight, { fit: 'inside' })
.toFormat(variant.format, { quality: variant.quality })
.toBuffer();
const key = `media/${imageKey}/${variant.name}.${variant.format}`;
await this.storage.put(key, processed);
results.push({
variant: variant.name,
url: `https://pbs.twimg.com/${key}`,
width: processed.width,
height: processed.height,
size: processed.length
});
}
// Atualizar tweet com URLs de mídia processadas
await this.tweetStore.updateMediaUrls(imageKey, results);
return results;
}
}
// Padrões de chave de cache para diferentes entidades
const cacheKeys = {
// Cache de timeline: Lista Redis de tweet IDs
timeline: (userId: string) => `tl:${userId}`,
// Cache de tweet: Hash dos dados do tweet
tweet: (tweetId: string) => `tw:${tweetId}`,
// Cache de perfil de usuário
user: (userId: string) => `u:${userId}`,
// Contagem de seguidores (acessada frequentemente, muda raramente)
followerCount: (userId: string) => `fc:${userId}`,
// Grafo social: Set de IDs seguidos
following: (userId: string) => `fg:${userId}`,
// Contagens de engajamento do tweet (atualizado pelo serviço de contadores)
tweetCounts: (tweetId: string) => `tc:${tweetId}`,
// Trending topics por localização
trending: (locationId: string) => `tr:${locationId}`,
// Contadores de rate limit
rateLimit: (userId: string, action: string) => `rl:${userId}:${action}`,
};
// Estratégia de TTL do cache
const cacheTTLs = {
timeline: 0, // Sem expiração (atualizado pelo fan-out)
tweet: 24 * 3600, // 24 horas
user: 3600, // 1 hora
followerCount: 300, // 5 minutos
following: 3600, // 1 hora (invalidado em follow/unfollow)
tweetCounts: 60, // 1 minuto (atualizado frequentemente)
trending: 60, // 1 minuto
rateLimit: 900, // 15 minutos (corresponde à janela de rate limit)
};
Quando o tweet de uma celebridade viraliza, a chave de cache do tweet se torna uma "hot key" que recebe milhões de leituras por segundo, sobrecarregando um único nó de Redis.
class HotKeyHandler {
private readonly HOT_THRESHOLD = 10_000; // leituras por segundo
async get(key: string): Promise<string | null> {
// Verificar se é uma hot key conhecida
if (this.hotKeys.has(key)) {
// Ler do cache local em processo primeiro
const local = this.localCache.get(key);
if (local) return local;
// Se não estiver no cache local, ler de uma réplica aleatória
// (distribui a carga entre múltiplos nós Redis)
const replica = this.selectRandomReplica();
const value = await replica.get(key);
// Cachear localmente por 5 segundos
this.localCache.set(key, value, { ttl: 5 });
return value;
}
// Caminho normal para chaves não-hot
return this.redis.get(key);
}
// Processo em background monitora a frequência de acesso a chaves
async monitorHotKeys(): Promise<void> {
setInterval(async () => {
const topKeys = await this.redis.call('HOTKEYS'); // Funcionalidade do Redis 7+
for (const [key, frequency] of topKeys) {
if (frequency > this.HOT_THRESHOLD) {
this.hotKeys.add(key);
// Replicar para todos os nós Redis
await this.replicateToAllNodes(key);
}
}
}, 10_000); // Verificar a cada 10 segundos
}
}
// Sharding do Tweet Store: por user_id
// Justificativa: Todos os tweets de um usuário ficam no mesmo shard
// Habilita consultas eficientes "buscar tweets do usuário"
class TweetShardRouter {
private readonly SHARD_COUNT = 4096;
getShard(userId: string): number {
// Consistent hashing com nós virtuais
const hash = murmurHash3(userId);
return hash % this.SHARD_COUNT;
}
// Para busca de tweet por tweet_id:
// Extrair user_id dos metadados do Snowflake ID
// OU manter um cache de mapeamento tweet_id -> shard
getShardByTweetId(tweetId: string): number {
// O Snowflake ID contém informações de datacenter + worker
// Podemos derivar o shard a partir disso
const userId = this.tweetMetadata.getUserId(tweetId);
return this.getShard(userId);
}
}
// Sharding do Grafo Social: por user_id
// Lista de adjacência: follower_id -> [following_ids]
// Lista reversa: following_id -> [follower_ids]
class SocialGraphShardRouter {
// Ambos os grafos (direto e reverso) são shardados pelo usuário "origem"
getFollowingShard(userId: string): number {
return murmurHash3(`following:${userId}`) % this.SHARD_COUNT;
}
getFollowersShard(userId: string): number {
return murmurHash3(`followers:${userId}`) % this.SHARD_COUNT;
}
}
Tweet Store (Cassandra):
- Fator de Replicação: 3
- Nível de Consistência:
- Escritas: QUORUM (2 de 3)
- Leituras: ONE (leituras rápidas, consistência eventual OK)
- Cross-datacenter: EACH_QUORUM para escritas
User Store (PostgreSQL):
- Primário + 2 réplicas síncronas
- Réplicas de leitura em cada região (assíncrono)
- Failover: automático com Patroni
Timeline Cache (Redis):
- Redis Cluster com 3 réplicas por master
- Cross-region: clusters independentes (reconstruídos a partir do Kafka em failover)
Search Index (Elasticsearch):
- 3 réplicas por shard
- Cross-region: clusters independentes, indexados a partir do Kafka
class CircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastFailureTime = 0;
private readonly FAILURE_THRESHOLD = 5;
private readonly RESET_TIMEOUT_MS = 30_000;
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.RESET_TIMEOUT_MS) {
this.state = 'HALF_OPEN';
} else {
throw new CircuitOpenError('Circuit breaker is open');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.FAILURE_THRESHOLD) {
this.state = 'OPEN';
}
}
}
// Uso no Timeline Service
class ResilientTimelineService {
private searchCircuitBreaker = new CircuitBreaker();
private recommendationCircuitBreaker = new CircuitBreaker();
async getTimeline(userId: string): Promise<Tweet[]> {
// Timeline principal sempre funciona (Redis)
const timeline = await this.timelineCache.get(userId);
// Recomendações baseadas em busca: opcional, protegida por circuit breaker
let recommendations: Tweet[] = [];
try {
recommendations = await this.recommendationCircuitBreaker.execute(
() => this.recommendationService.getForUser(userId)
);
} catch {
// Degradação graciosa: timeline funciona sem recomendações
this.metrics.increment('recommendation_circuit_open');
}
return this.merge(timeline, recommendations);
}
}
Nível 0 (Serviço Completo):
Timeline cronológica
Ranking algorítmico
Recomendações
Trending topics
Busca
Notificações
Mídia
Nível 1 (Degradação Menor):
Timeline cronológica
Ranking algorítmico -> fallback para cronológico
Recomendações -> esconde "Quem seguir"
Trending topics
Busca
Notificações
Mídia
Nível 2 (Degradação Significativa):
Timeline cronológica
Ranking algorítmico desabilitado
Recomendações desabilitadas
Trending topics -> mostra trending cacheado
Busca -> "Busca temporariamente indisponível"
Notificações (atrasadas)
Mídia (apenas cacheada)
Nível 3 (Emergência):
Timeline somente leitura (cacheada)
Não é possível postar novos tweets
Todas as funcionalidades não essenciais desabilitadas
DMs (infraestrutura separada)
interface TwitterSLIs {
// Timeline
timeline_latency_p50: number; // Meta: < 50ms
timeline_latency_p99: number; // Meta: < 200ms
timeline_availability: number; // Meta: 99.99%
// Criação de tweets
tweet_create_latency_p99: number; // Meta: < 500ms
tweet_create_error_rate: number; // Meta: < 0.01%
// Fan-out
fanout_lag_seconds: number; // Meta: < 5s para usuários normais
fanout_lag_p99: number; // Meta: < 30s
kafka_consumer_lag: number; // Meta: < 10,000 mensagens
// Busca
search_latency_p99: number; // Meta: < 500ms
search_index_lag: number; // Meta: < 30s
// Infraestrutura
redis_hit_rate: number; // Meta: > 99%
cache_eviction_rate: number; // Meta: < 0.1%
db_connection_pool_usage: number; // Meta: < 80%
kafka_partition_lag: number; // Meta: < 5,000
}
// Cada requisição recebe um trace ID que a acompanha por todos os serviços
interface TraceSpan {
traceId: string; // Único em todo o sistema
spanId: string; // Único dentro do trace
parentSpanId?: string; // Referência ao span pai
service: string; // "timeline-service", "tweet-store", etc.
operation: string; // "getTimeline", "fanOut", etc.
startTime: number;
duration: number;
tags: Record<string, string>;
logs: Array<{ timestamp: number; message: string }>;
}
// Exemplo de trace para uma requisição de timeline:
//
// [API Gateway: 150ms total]
// +-- [Auth Service: 5ms]
// +-- [Timeline Service: 120ms]
// | +-- [Redis GET timeline:user123: 2ms]
// | +-- [Tweet Store multiGet: 15ms]
// | | +-- [Cassandra query shard-1: 8ms]
// | | +-- [Cassandra query shard-2: 12ms]
// | +-- [Celebrity tweets fetch: 25ms]
// | | +-- [Social Graph: getCelebrities: 3ms]
// | | +-- [Tweet Store: 20ms]
// | +-- [Ranking Service: 30ms]
// | +-- [ML Model inference: 25ms]
// +-- [Response serialization: 5ms]
Minutos 0-5: Requisitos e Escala (NÃO PULE ESTA ETAPA)
- Esclarecer requisitos funcionais
- Discutir números de escala
- Cálculos de envelope
- Ponto chave: insight da proporção leitura:escrita
Minutos 5-15: Design de Alto Nível
- Desenhar o diagrama de arquitetura
- Identificar serviços principais
- Explicar o fluxo de dados para post e leitura
Minutos 15-30: Deep Dive (O NÚCLEO)
- Geração da timeline: fan-out on write vs read
- Abordagem híbrida para celebridades
- Arquitetura de cache da timeline
- É aqui que você se diferencia
Minutos 30-40: Sistemas de Suporte
- Escolhas de banco de dados e sharding
- Estratégia de cache
- Busca (se houver tempo)
Minutos 40-45: Confiabilidade e Encerramento
- Tolerância a falhas
- Monitoramento
- Considerações futuras
| Decisão | Opção A | Opção B | Escolha do Twitter |
|---|---|---|---|
| Timeline | Fan-out on Write | Fan-out on Read | Híbrido |
| Consistência | Forte | Eventual | Eventual (timeline), Forte (DMs) |
| Geração de ID | Auto-increment | Snowflake | Snowflake |
| Tweet Store | SQL | NoSQL | NoSQL (Manhattan) |
| Grafo Social | Relacional | Graph DB | Graph DB (FlockDB) |
| Fila de Mensagens | RabbitMQ | Kafka | Kafka |
| Cache | Memcached | Redis | Redis (+ Memcached para alguns) |
Errado: Começar pelo schema do banco de dados antes de entender os requisitos
Correto: Começar com requisitos, depois design de alto nível, depois deep dive
Errado: Tentar cobrir tudo igualmente
Correto: Ir fundo no problema central (geração de timeline) e mencionar os demais brevemente
Errado: Dizer "vamos usar um load balancer" sem explicar o porquê
Correto: Explicar a decisão: "Load balancer L7 para roteamento baseado em conteúdo para diferentes clusters de serviço"
Errado: Ignorar os números e fazer argumentos vagos
Correto: Calcular: "Com 6.000 tweets/seg e média de 200 seguidores, são 1,2M de escritas de fan-out/seg"
Errado: Fan-out on write para TODOS os usuários
Ruim: Todo tweet sofre fan-out para todos os seguidores, incluindo celebridades com 100M+ seguidores
Custo: Um único tweet de celebridade dispara 100M+ escritas, levando horas
Correto: Abordagem híbrida com detecção de celebridades
Bom: Usuários normais usam fan-out on write, celebridades usam fan-out on read
Resultado: Distribuição uniforme do trabalho, entrega de timeline em sub-segundo
Errado: Usar um único banco de dados para tudo
Ruim: PostgreSQL para tweets, timelines, busca, analytics e grafo social
Resultado: Tudo falha junto, não consegue escalar independentemente
Correto: Persistência poliglota
Bom: Banco de dados certo para cada carga de trabalho
- Cassandra para tweets (alto throughput de escrita)
- PostgreSQL para usuários (consistência forte)
- Redis para timelines (leituras de baixa latência)
- Elasticsearch para busca (full-text)
Errado: Fan-out síncrono no caminho da requisição
Ruim: POST /tweet -> esperar fan-out para 10.000 seguidores -> retornar resposta
Resultado: 5 segundos de latência para postar um tweet
Correto: Fan-out assíncrono via fila de mensagens
Bom: POST /tweet -> armazenar tweet -> publicar no Kafka -> retornar imediatamente
Fan-out acontece de forma assíncrona, usuário vê resposta instantânea
Projetar o Twitter/X.com é uma aula magistral em trade-offs de sistemas distribuídos. Os insights arquiteturais chave são:
A assimetria leitura-escrita (500:1) direciona toda decisão importante. Pré-computar timelines (fan-out on write) troca armazenamento e amplificação de escrita por leituras extremamente rápidas.
A abordagem híbrida de fan-out resolve elegantemente o problema das celebridades combinando o melhor de ambas as estratégias -- fan-out on write para usuários normais, fan-out on read para celebridades.
Snowflake IDs resolvem a geração de IDs distribuídos sem coordenação, ao mesmo tempo fornecendo ordenação temporal que elimina a necessidade de índices de timestamp separados.
Persistência poliglota permite que cada padrão de acesso a dados use a melhor ferramenta -- Cassandra para escritas de alto throughput, Redis para leituras de baixa latência, Elasticsearch para busca full-text.
Processamento assíncrono via Kafka desacopla o caminho rápido de escrita da maquinaria complexa de fan-out, indexação e notificações.
A evolução do Twitter de um monolito Ruby on Rails para um dos sistemas distribuídos mais sofisticados do mundo levou mais de uma década. Entender essas decisões e seus trade-offs é o que separa uma boa resposta de system design de uma excelente.
Da próxima vez que você postar um tweet e ele aparecer em milhões de timelines em segundos, você vai saber a engenharia extraordinária que torna isso possível.
| Métrica | Valor |
|---|---|
| DAU | 250M |
| Tweets/dia | 500M |
| Tweets/segundo (média) | 6.000 |
| Leituras de timeline/segundo | 3M |
| Proporção Leitura:Escrita | 500:1 |
| Média de seguidores | 200 |
| Limiar de celebridade | 100K+ seguidores |
| Tamanho do cache de timeline | 800 tweet IDs por usuário |
| Tamanho do tweet (texto+meta) | ~300 bytes |
| Bits do Snowflake ID | 64 (41 tempo + 5 DC + 5 worker + 12 seq) |
| Componente | Tecnologia | Por Quê |
|---|---|---|
| Tweet Store | Cassandra/Manhattan | Alto throughput de escrita, séries temporais |
| User Store | PostgreSQL | Consistência forte, consultas complexas |
| Grafo Social | FlockDB/Neo4j | Travessias de grafo, listas de adjacência |
| Timeline Cache | Redis Cluster | Leituras sub-ms, estrutura de dados de lista |
| Fila de Mensagens | Apache Kafka | Durável, ordenado, alto throughput |
| Índice de Busca | Elasticsearch | Busca full-text, índice invertido |
| Geração de ID | Snowflake | Distribuído, ordenável por tempo, sem coordenação |
| Armazenamento de Mídia | S3/HDFS | Blob storage, integração com CDN |
| CDN | CloudFront/Akamai | Entrega global de mídia |
| API Gateway | Custom (Finagle) | Roteamento L7, rate limiting |
Novo tweet postado
+-- Verificar contagem de seguidores do autor
+-- < 100K seguidores -> Fan-Out on Write
| +-- Inserir tweet_id na timeline Redis de cada seguidor
+-- >= 100K seguidores -> Fan-Out on Read
+-- Armazenar no cache de tweets de celebridades
+-- Combinado no momento da leitura com timeline pré-computada
Para ajudar você a navegar pela terminologia utilizada ao longo deste artigo, segue um glossário com os termos mais importantes.
| Termo | Definição |
|---|---|
| Fan-Out | O processo de distribuir um único tweet para as timelines de múltiplos seguidores. Pode acontecer na escrita (write) ou na leitura (read). |
| Hot Key | Uma chave no cache que recebe um volume desproporcional de acessos, potencialmente sobrecarregando um único nó do cluster. |
| Snowflake ID | Sistema de geração de IDs distribuídos criado pelo Twitter. Gera IDs únicos de 64 bits que são ordenáveis por tempo sem coordenação central. |
| Consistência Eventual | Modelo de consistência em que as réplicas eventualmente convergem para o mesmo estado, mas leituras imediatas podem retornar dados desatualizados. |
| Sharding | Técnica de particionar dados horizontalmente entre múltiplos servidores para escalar além dos limites de uma única máquina. |
| Circuit Breaker | Padrão de design que impede chamadas repetidas a serviços falhos, permitindo que se recuperem antes de receber novas requisições. |
| Persistência Poliglota | Estratégia de usar diferentes tecnologias de banco de dados para diferentes padrões de acesso a dados dentro do mesmo sistema. |
| Índice Invertido | Estrutura de dados de busca que mapeia termos para os documentos que os contêm, permitindo consultas de texto completo eficientes. |
| BM25 | Algoritmo de ranking utilizado em motores de busca para classificar documentos por relevância em relação a uma consulta. |
| Consistent Hashing | Técnica de distribuição que minimiza o remapeamento de dados quando nós são adicionados ou removidos do cluster. |
| QUORUM | Nível de consistência que exige que a maioria das réplicas confirme uma operação antes de considerá-la bem-sucedida. |
| SLI (Service Level Indicator) | Métrica quantitativa que mede um aspecto específico do nível de serviço fornecido, como latência ou disponibilidade. |
| CDN (Content Delivery Network) | Rede de servidores distribuídos geograficamente que entrega conteúdo estático (imagens, vídeos) a partir do ponto mais próximo ao usuário. |
| Rate Limiting | Técnica que limita o número de requisições que um usuário ou aplicação pode fazer em um determinado período de tempo. |
| Degradação Graciosa | Capacidade de um sistema continuar operando com funcionalidade reduzida em vez de falhar completamente quando componentes apresentam problemas. |
Para consolidar seu entendimento, vamos explorar três cenários do mundo real que exercitam múltiplos componentes da arquitetura simultaneamente.
Imagine a final da Copa do Mundo. O Brasil marca o gol da vitória nos acréscimos. O que acontece nos bastidores?
Pico de tweets: O volume de tweets salta de 6.000/seg para 150.000/seg em uma janela de 30 segundos. O sistema precisa lidar com esse burst sem perder dados.
Como a arquitetura responde:
Kafka absorve o pico: Com 128 partições no tópico tweet-events, cada partição precisa processar cerca de 1.170 mensagens/seg -- bem dentro da capacidade.
Fan-out priorizado: O serviço de fan-out prioriza tweets de contas verificadas e jornalistas esportivos, garantindo que informações relevantes cheguem primeiro.
Trending explode: O algoritmo de trending detecta a velocidade anômala das hashtags relacionadas e as promove instantaneamente.
CDN sob pressão: Milhões de GIFs do gol são compartilhados. O CDN precisa servir terabytes de mídia. A estratégia de pre-warming dos edges mais próximos das regiões afetadas é crucial.
Degradação preventiva: O sistema pode desativar recursos não essenciais (recomendações de "Quem seguir", analytics em tempo real) para priorizar o caminho crítico: postagem e leitura de timeline.
Quando uma celebridade com 50 milhões de seguidores muda seu nome de exibição, isso gera um desafio de invalidação de cache massiva.
O problema: O nome antigo está cacheado em milhões de timelines renderizadas, perfis hidratados e notificações.
Como resolver:
Atualização no banco de dados primário: O PostgreSQL atualiza atomicamente o campo display_name.
Invalidação de cache do perfil: A chave u:{userId} no Redis é invalidada imediatamente.
Propagação eventual: As timelines não são atualizadas retroativamente. Tweets já renderizados em cache mostrarão o nome antigo até que o cache expire naturalmente (TTL de 24h para tweets) ou até que o usuário faça refresh.
Cache de perfil atualizado sob demanda: A próxima leitura do perfil busca o dado atualizado do PostgreSQL e preenche o cache novamente.
Esta é uma demonstração perfeita de consistência eventual em ação -- e por que ela é aceitável para uma plataforma como o Twitter.
Um dos três datacenters do Twitter fica indisponivel devido a uma falha de energia.
Impacto imediato:
Mecanismo de recuperação:
Load Balancer detecta falha: Health checks falham em < 10 segundos. O tráfego é redirecionado para os dois datacenters restantes.
Kafka faz leader election: As partições do Kafka elegem novos leaders nos datacenters sobreviventes. O ISR (In-Sync Replicas) garante zero perda de dados.
Redis Cluster rebalanceia: Os Redis replicas nos datacenters sobreviventes são promovidos a masters. Timelines que estavam no datacenter falho são reconstruídas a partir do Kafka event log.
Cassandra continua servindo: Com QUORUM consistency level, as leituras e escritas continuam funcionando desde que 2 de 3 réplicas estejam disponíveis.
Capacidade reduzida: O sistema opera com 66% da capacidade total. Se a carga exceder esse limite, a degradação graciosa é ativada (desabilitar ranking, recomendações, etc.).
Entender como o Twitter se compara com outras plataformas sociais ajuda a contextualizar as decisões arquiteturais.
| Aspecto | ||
|---|---|---|
| Conteúdo primário | Texto curto | Imagens/Vídeo |
| Modelo de timeline | Híbrido fan-out | Primariamente fan-out on write |
| Desafio principal | Assimetria de leitura/escrita | Processamento e armazenamento de mídia |
| Tamanho médio do post | ~300 bytes | ~2 MB (com imagem) |
| Grafo social | Assimétrico (seguidores) | Assimétrico (seguidores) |
| Busca | Full-text em todos os posts | Limitada a hashtags e usuários |
| Tempo real | Crítico (notícias) | Menos crítico (entretenimento) |
| Aspecto | ||
|---|---|---|
| Grafo social | Dirigido (follow) | Não-dirigido (amizade) |
| Escopo do conteúdo | Público por padrão | Privado por padrão |
| Problema de celebridade | Severo (100M+ seguidores) | Moderado (5K amigos máximo) |
| Ranking | Cronológico + ML | Primariamente ML (EdgeRank) |
| Modelo de dados | Simples (texto curto) | Complexo (posts, fotos, eventos, grupos) |
| Fan-out | Híbrido | Primariamente fan-out on read (pull model) |
1. Edge Computing para Timeline
Em vez de servir timelines de datacenters centralizados, a tendência é pré-computar e cachear timelines em pontos de presença (PoPs) mais próximos dos usuários. Isso pode reduzir a latência de leitura de 50ms para menos de 10ms.
2. Machine Learning no Caminho Crítico
O ranking de timeline já utiliza ML, mas a tendência é integrar modelos de ML cada vez mais sofisticados diretamente no caminho de serviço, incluindo:
Usuário -> API Gateway -> Validação/Spam -> Tweet Service -> Tweet Store (Cassandra)
-> Kafka (tweet-events)
|
+---------------------+-------------------+
| | |
Fan-Out Service Search Indexer Notification Service
| | |
Timeline Cache Elasticsearch Push/WebSocket
(Redis)
Usuário -> CDN (mídia) -> API Gateway -> Timeline Service
|
+---------+---------+
| |
Timeline Cache Celebrity Tweet Cache
(Redis) (Redis)
| |
+--------+----------+
|
Timeline Mixer
|
Ranking Service (ML)
|
Tweet Store (hidratar dados)
|
Resposta ao Usuário
Sobre o autor: Este artigo faz parte de uma série aprofundada sobre System Design para engenheiros de software. Última atualização: Fevereiro 2026