Enunciado del Problema
Diseña un sistema de feed de redes sociales similar a Twitter o Instagram. Los usuarios pueden crear publicaciones, seguir a otros usuarios y ver un timeline personalizado de publicaciones de las personas que siguen. Esta es una de las preguntas de diseño de sistemas más populares y complejas porque involucra datos en tiempo real, escala masiva y estrategias sofisticadas de caché.
Paso 1: Requisitos
Requisitos Funcionales
- Los usuarios pueden crear publicaciones de texto (tweets) con media opcional
- Los usuarios pueden seguir/dejar de seguir a otros usuarios
- Los usuarios pueden ver su timeline de inicio (publicaciones de quienes siguen)
- Los usuarios pueden ver el timeline del perfil de un usuario específico
- Las publicaciones aparecen en orden cronológico inverso (con ranking opcional)
Requisitos No Funcionales
- La generación del timeline debe ser rápida (<200ms)
- Alta disponibilidad (el feed es la experiencia central)
- La consistencia eventual es aceptable (que una publicación aparezca unos segundos tarde está bien)
- Soporte para 500 millones de usuarios, 200 millones de usuarios activos diarios
Estimaciones de Escala
| Métrica | Estimación |
|---|---|
| Usuarios activos diarios | 200 millones |
| Publicaciones promedio por usuario/día | 2 |
| Nuevas publicaciones por día | 400 millones |
| Seguidores promedio por usuario | 200 |
| Vistas de timeline por usuario/día | 5 |
| Lecturas de timeline por día | 1 mil millones |
Paso 2: Modelo de Datos
// Modelos de datos principales
interface User {
id: string;
username: string;
displayName: string;
avatarUrl: string;
followerCount: number;
followingCount: number;
createdAt: Date;
}
interface Post {
id: string; // Snowflake ID (contiene timestamp)
userId: string;
content: string;
mediaUrls: string[]; // Imágenes, videos almacenados en object storage
likeCount: number;
repostCount: number;
replyCount: number;
createdAt: Date;
}
interface Follow {
followerId: string; // El usuario que sigue
followeeId: string; // El usuario siendo seguido
createdAt: Date;
}
// Entrada de timeline (cacheada en Redis)
interface TimelineEntry {
postId: string;
userId: string;
timestamp: number; // Para ordenamiento
}
Paso 3: Fan-out en Escritura vs Fan-out en Lectura
Esta es la decisión de diseño más crítica para un feed de redes sociales. Cuando un usuario publica una publicación, ¿cómo termina en los timelines de sus seguidores?
Fan-out en Escritura (Modelo Push)
Cuando un usuario publica una publicación, el sistema inmediatamente empuja la publicación al caché de timeline de cada seguidor. El timeline de cada usuario está pre-computado y almacenado en Redis.
Fan-out en Lectura (Modelo Pull)
Cuando un usuario abre su timeline, el sistema jala publicaciones de todos los usuarios que sigue, las fusiona y ordena por tiempo. No hay pre-computación en tiempo de escritura.
Comparación de Estrategias de Fan-out
| Aspecto | Fan-out en Escritura | Fan-out en Lectura |
|---|---|---|
| Costo de Escritura | Alto (push a N seguidores) | Bajo (escribir publicación una vez) |
| Costo de Lectura | Bajo (timeline pre-computado) | Alto (fusionar N feeds en tiempo real) |
| Latencia de Timeline | Lecturas muy rápidas | Lecturas más lentas |
| Problema de Celebridades | Millones de escrituras por publicación | Sin problema (se jala bajo demanda) |
| Usuarios Inactivos | Escrituras desperdiciadas para usuarios que nunca revisan | Sin trabajo desperdiciado |
| Consistencia | Publicación visible después de completar fan-out | Siempre actualizado |
El Enfoque Híbrido (Lo Que Twitter Realmente Usa)
Twitter usa un modelo híbrido. La mayoría de los usuarios usan fan-out en escritura. Las cuentas de celebridades (usuarios con millones de seguidores) usan fan-out en lectura. Cuando abres tu timeline, fusiona tu timeline cacheado pre-computado con obtenciones en tiempo real de cuentas de celebridades que sigues.
const CELEBRITY_THRESHOLD = 50_000; // Umbral de seguidores
class FeedService {
private cache: RedisClient;
private db: Database;
private messageQueue: MessageQueue;
// Cuando un usuario crea una publicación
async publishPost(userId: string, post: Post): Promise<void> {
// Guardar la publicación en la base de datos
await this.db.posts.insert(post);
const followerCount = await this.db.getFollowerCount(userId);
if (followerCount < CELEBRITY_THRESHOLD) {
// Fan-out en escritura: empujar a timelines de seguidores
await this.messageQueue.publish("fanout", {
postId: post.id,
userId,
timestamp: post.createdAt.getTime(),
});
}
// Celebridades: sus publicaciones se obtienen en lectura (fan-out en lectura)
}
// El worker de fan-out procesa
async processFanout(event: FanoutEvent): Promise<void> {
const followers = await this.db.getFollowerIds(event.userId);
// Empujar al caché de timeline de cada seguidor (Redis sorted set)
const pipeline = this.cache.pipeline();
for (const followerId of followers) {
pipeline.zadd(`timeline:${followerId}`, event.timestamp, event.postId);
// Recortar timeline para mantener solo las últimas 800 publicaciones
pipeline.zremrangebyrank(`timeline:${followerId}`, 0, -801);
}
await pipeline.exec();
}
// Cuando un usuario ve su timeline
async getTimeline(userId: string, page = 0, pageSize = 20): Promise<Post[]> {
const start = page * pageSize;
const end = start + pageSize - 1;
// Obtener entradas de timeline pre-computadas del caché
const cachedPostIds = await this.cache.zrevrange(
`timeline:${userId}`,
start,
end
);
// Obtener publicaciones de celebridades que este usuario sigue (fan-out en lectura)
const celebrities = await this.getCelebritiesFollowed(userId);
const celebrityPosts = await this.getRecentPostsFromUsers(celebrities);
// Fusionar y ordenar
const allPostIds = this.mergeAndSort(cachedPostIds, celebrityPosts);
// Obtener objetos completos de publicaciones
return this.db.posts.findByIds(allPostIds.slice(0, pageSize));
}
}
Paso 4: Estrategias de Caché
El caché es esencial para el rendimiento del feed. Con miles de millones de lecturas de timeline por día, necesitamos una estrategia de caché multi-capa.
Qué Cachear
- Caché de Timeline (Redis Sorted Set): El timeline de cada usuario como un sorted set de IDs de publicación con puntuación por timestamp. Mantiene las últimas 800 entradas.
- Caché de Publicaciones: Objetos completos de publicaciones cacheados por ID. Evita lecturas a la base de datos para publicaciones populares.
- Caché de Usuarios: Datos de perfil de usuario (nombre, avatar) cacheados ya que aparecen en cada publicación del feed.
- Caché de Grafo Social: Listas de seguidores/seguidos cacheadas para búsquedas rápidas durante el fan-out.
- Caché de Contadores: Contadores de likes y reposts actualizados via incrementos atómicos de Redis.
// Estructuras de datos Redis para caché de feed
class FeedCache {
// Timeline: Sorted Set (IDs de publicación con puntuación por timestamp)
// Key: timeline:{userId}
// Score: timestamp
// Member: postId
async addToTimeline(userId: string, postId: string, timestamp: number) {
await redis.zadd(`timeline:${userId}`, timestamp, postId);
await redis.zremrangebyrank(`timeline:${userId}`, 0, -801); // Mantener 800 máx
}
// Caché de publicaciones: Hash
// Key: post:{postId}
async cachePost(post: Post) {
await redis.hset(`post:${post.id}`, {
userId: post.userId,
content: post.content,
likeCount: post.likeCount.toString(),
createdAt: post.createdAt.toISOString(),
});
await redis.expire(`post:${post.id}`, 86400); // TTL 24h
}
// Caché de usuarios: Hash
// Key: user:{userId}
async cacheUser(user: User) {
await redis.hset(`user:${user.id}`, {
username: user.username,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
});
await redis.expire(`user:${user.id}`, 3600); // TTL 1h
}
}
Paso 5: Almacenamiento de Media y CDN
Las publicaciones frecuentemente contienen imágenes y videos que son significativamente más grandes que el texto. El media requiere un pipeline separado de almacenamiento y entrega.
- Object Storage: Almacenar archivos de media originales en S3 o GCS
- Procesamiento de Imágenes: Generar miniaturas y múltiples resoluciones asincrónicamente
- CDN: Servir media a través de un CDN para entrega de baja latencia a nivel mundial
- Lazy Loading: Cargar media solo cuando la publicación entra en la vista para reducir ancho de banda
Paso 6: Consideraciones de Escalado
Escalado de Base de Datos
- Tabla de publicaciones: Shard por userId (todas las publicaciones de un usuario en el mismo shard)
- Tabla de follows: Shard por followerId para consultas eficientes de "¿a quién sigo?"
- Réplicas de lectura: Para lecturas de timeline de perfil y consultas de analíticas
Ranking del Feed
Los feeds de redes sociales modernos van más allá del orden cronológico inverso. Un algoritmo de ranking considera factores como historial de interacción del usuario, recencia de la publicación, tipo de contenido y proximidad social para mostrar el contenido más relevante.
// Ranking simplificado del feed
interface RankedPost {
post: Post;
score: number;
}
function rankPosts(posts: Post[], userId: string, userContext: UserContext): RankedPost[] {
return posts
.map((post) => ({
post,
score: calculateRelevanceScore(post, userId, userContext),
}))
.sort((a, b) => b.score - a.score);
}
function calculateRelevanceScore(
post: Post,
viewerId: string,
context: UserContext
): number {
let score = 0;
// Recencia: decaimiento con el tiempo
const ageHours = (Date.now() - post.createdAt.getTime()) / 3600000;
score += Math.max(0, 100 - ageHours * 2); // Pierde 2 puntos por hora
// Señales de engagement
score += Math.log(post.likeCount + 1) * 10;
score += Math.log(post.replyCount + 1) * 15; // Respuestas valen más
score += Math.log(post.repostCount + 1) * 12;
// Proximidad social (qué tan cercano es el autor al espectador)
const closeness = context.interactionFrequency[post.userId] || 0;
score += closeness * 20;
// Bonus por tipo de contenido
if (post.mediaUrls.length > 0) score += 5; // Publicaciones con media rankean ligeramente más alto
return score;
}
Resumen de Arquitectura
- Ruta de Escritura: Cliente -> API Gateway -> Servicio de Publicaciones -> BD + Cola de Mensajes -> Workers Fan-out -> Timelines Redis
- Ruta de Lectura: Cliente -> API Gateway -> Servicio de Feed -> Timeline Redis + Publicaciones de Celebridades -> Fusionar + Rankear -> Retornar
- Ruta de Media: Cliente -> Servicio de Subida -> S3 -> Cola de Procesamiento -> Miniaturas -> CDN
- Idea Clave: El enfoque híbrido de fan-out es la mejor práctica aceptada. Siempre menciona esto en entrevistas.