Introducción
Vas a una entrevista de sistema distribuido y te preguntan: "Diseñá un sistema de mensajería para 100 millones de usuarios". El silencio que sigue puede costar el trabajo.
Las entrevistas de system design son las que más candidatos eliminan en FAANG, unicornios y empresas que pagan en dólares — no porque sean imposibles, sino porque nadie explica cómo estructurar el pensamiento. Este artículo cambia eso.
Cubrimos 35+ preguntas reales organizadas por categoría, con respuestas detalladas, código real en Python/Go/pseudo-código, y los trade-offs que los entrevistadores realmente esperan que menciones.
Cómo funcionan estas entrevistas (antes de las preguntas)
Antes de memorizar respuestas, entendé el juego. El entrevistador no busca la respuesta correcta — busca cómo pensás bajo presión.
El framework que funciona:
- Clarificá el scope (5 min): usuarios, escala, latencia aceptable, consistencia requerida
- Estimá los números (3 min): QPS, storage, ancho de banda
- Diseñá el happy path (10 min): arquitectura de alto nivel
- Deep dive en componentes críticos (15 min): el entrevistador te va a pedir que profundices en uno
- Identificá y discutí bottlenecks (5 min): single points of failure, hotspots
Fundamentos: preguntas que siempre vuelven
1. ¿Qué es un sistema distribuido y cuáles son sus principales desafíos?
Un sistema distribuido es un conjunto de nodos independientes que colaboran para el usuario final como si fuera uno solo. Los desafíos son:
- Network partitions: los nodos se desconectan entre sí. No podés asumir que un mensaje llegó.
- Latencia no determinística: un llamado que tarda 5 ms hoy puede tardar 2 segundos mañana.
- Consistencia parcial: dos nodos pueden tener versiones diferentes del mismo dato al mismo tiempo.
- Fallos silenciosos: un nodo puede caerse sin avisar.
La respuesta que distingue a los candidatos buenos: no solo nombran estos problemas, sino que explican cómo los sistemas concretos los mitigan (idempotencia, heartbeats, reintento con backoff exponencial).
2. Explicá el Teorema CAP
El teorema CAP dice que en presencia de una partición de red (P), un sistema distribuido solo puede garantizar consistencia (C) o disponibilidad (A), no ambas simultáneamente.
- CP (Consistency + Partition tolerance): el sistema rechaza requests si no puede garantizar datos actualizados. Ejemplo: HBase, ZooKeeper.
- AP (Availability + Partition tolerance): el sistema responde siempre, aunque con datos posiblemente desactualizados. Ejemplo: Cassandra, DynamoDB en modo eventual.
La trampa de la entrevista: CAP asume que una partición ya ocurrió. En la práctica, elegís tu trade-off en diseño, no solo durante la falla. La mayoría de los sistemas son AP con consistencia eventual configurable.
Lo que el entrevistador quiere oir: "Para este caso particular, elijo AP porque los usuarios prefieren ver datos ligeramente desactualizados a recibir un error." Eso es pensamiento de producto + sistemas.
3. ¿Qué es consistencia eventual y cuándo la usás?
Consistencia eventual significa que si dejás de escribir en un sistema, todos los nodos eventualmente van a converger al mismo valor. No hay garantía de cuándo, pero eventualmente ocurre.
La usás cuando:
- El costo del conflicto es bajo (likes en un post — si ves 1.204 o 1.205 no importa)
- La disponibilidad es prioritaria
- La partición de red es una posibilidad real
Ejemplo concreto: el contador de views de YouTube. No necesitás que sea exacto al segundo. DNS también usa consistencia eventual — actualizás un registro y tarda horas en propagarse, y está bien.
Cuando NO la usás: inventario de ecommerce (no podés vender el mismo producto dos veces), saldos bancarios, seats de vuelos.
4. ¿Cómo diferenciás latencia de throughput?
- Latencia: tiempo de un request individual (p50, p95, p99). "Este endpoint tarda 45 ms en el p99."
- Throughput: cuántos requests puede manejar el sistema por unidad de tiempo. "El servicio procesa 50,000 RPS."
Son independientes. Un sistema puede tener alta latencia y alto throughput (batch processing), o baja latencia y bajo throughput (una Raspberry Pi respondiendo queries simples).
En la entrevista: siempre preguntá cuál optimizar. "¿Qué es más crítico para este feature: latencia p99 < 100ms, o procesar 1M eventos/hora?"
5. ¿Qué es un single point of failure y cómo lo eliminás?
Un SPOF es cualquier componente cuya caída tumba el sistema completo.
Estrategias de eliminación:
Antes (SPOF):
[Client] → [Single DB] → Error si la DB cae
Después (sin SPOF):
[Client] → [Load Balancer] → [App Server 1] ┐
→ [App Server 2] ├─ → [DB Primary]
→ [App Server N] ┘ ↓ replication
[DB Replica 1]
[DB Replica 2]Además del hardware: dependency injection para tests, circuit breakers para servicios externos, y health checks automáticos con reinicio.
Diseño de bases de datos y almacenamiento
6. ¿Cuándo usás SQL vs NoSQL?
No es una guerra religiosa — es un trade-off:
Usá SQL cuando:
- Los datos tienen estructura fija y relaciones complejas (JOINs necesarios)
- Necesitás transacciones ACID (órdenes, pagos)
- El esquema no va a cambiar mucho
Usá NoSQL cuando:
- Escala horizontal masiva (sharding nativo)
- Modelo de datos flexible o jerárquico (documentos, grafos)
- Latencia ultra-baja con acceso por key
- Event sourcing o time-series
Ejemplo de respuesta buena en entrevista: "Para los perfiles de usuario usaría PostgreSQL por las relaciones y ACID. Para el feed de actividad usaría Cassandra — es append-only, necesita alta disponibilidad, y los queries son siempre por user_id con orden temporal."
7. ¿Cómo diseñás el sharding de una base de datos?
Sharding es partir los datos horizontalmente entre múltiples nodos. Tres estrategias:
Range sharding: shard 1 tiene users 1-1M, shard 2 tiene 1M-2M. Simple pero crea hotspots si los últimos registros son los más activos.
Hash sharding: shard = hash(user_id) % num_shards. Distribución uniforme, pero un JOIN cross-shard es costoso.
Directory sharding: una tabla de lookup mapea cada clave a su shard. Flexibilidad total, pero el directory se convierte en un SPOF.
# Hash sharding básico
def get_shard(user_id: int, num_shards: int) -> int:
return hash(str(user_id)) % num_shards
# Consistent hashing (mejor para rebalanceo)
import hashlib
def consistent_hash(key: str, nodes: list[str]) -> str:
ring = sorted((hashlib.md5(n.encode()).hexdigest(), n) for n in nodes)
h = hashlib.md5(key.encode()).hexdigest()
for node_hash, node in ring:
if h <= node_hash:
return node
return ring[0][1] # wrap aroundEl problema que mencionás para diferenciarte: hotspot con celebrities o eventos virales. Un usuario con 50M followers genera 100x más tráfico que el promedio. Solución: sharding por contenido + read replicas para cuentas calientes.
8. ¿Qué es consistent hashing y por qué importa?
Cuando agregás o sacás un nodo en hash sharding tradicional, tenés que remapear casi todas las claves (num_keys / num_nodes no alcanza — en la práctica es mucho más). Con consistent hashing, solo remapeás las claves del nodo afectado.
El truco: poner los nodos en un "ring" de hashes. Cada key se asigna al nodo más cercano en sentido horario. Cuando sacás un nodo, sus keys van al siguiente nodo del ring.
Usado en: Amazon DynamoDB, Apache Cassandra, Memcached (ketama), CDNs.
9. Diseñá un sistema de caché distribuida
Las preguntas clásicas dentro de este tema:
¿Qué estrategia de caché usás?
- Cache-aside (lazy loading): la aplicación consulta el caché, si hay miss va a la DB y llena el caché. Simple pero el primer request siempre es lento.
- Write-through: cada write va a caché y DB simultáneamente. Consistencia fuerte, pero writes más lentos.
- Write-back: el write va solo al caché; la DB se actualiza asíncronamente. Riesgoso si el nodo cae antes de persistir.
¿Qué evictás cuando el caché está lleno?
- LRU (Least Recently Used): el más usado en la práctica. Redis lo implementa nativamente.
- LFU (Least Frequently Used): mejor para patrones estables, peor para patrones cambiantes.
- TTL fijo: simple, predecible. Bueno para datos con vida útil conocida (tokens, sesiones).
# LRU Cache en Python (entrevista clásica también)
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)Cache stampede: cuando el caché expira y miles de requests van a la DB al mismo tiempo. Solución: mutex lock en el primer request, o probabilistic early expiration.
10. ¿Qué es un índice de base de datos y cuándo no lo usás?
Un índice acelera las lecturas a cambio de más espacio y writes más lentos (el índice también se actualiza con cada insert/update).
No usás un índice cuando:
- La tabla es pequeña (el full scan es igual o más rápido)
- La columna tiene baja cardinalidad (booleano, status con 3 valores) — el índice apenas ayuda
- La tabla tiene muchos más writes que reads
- Ya tenés demasiados índices y los writes se volvieron el bottleneck
Un error común en entrevistas: decir "le pongo índice a todo para que sea rápido." Eso es una señal de alarma. Un senior sabe que cada índice tiene costo.
Comunicación y mensajería
11. ¿Cuándo usás comunicación síncrona vs asíncrona?
Síncrona (REST/gRPC): cuando necesitás la respuesta inmediata para continuar. Login, checkout, cualquier query que el usuario espera.
Asíncrona (colas, eventos): cuando el procesamiento puede ocurrir después sin impactar la experiencia del usuario. Envío de emails, generación de reportes, procesamiento de imágenes, notificaciones push.
Regla práctica: si el usuario ve un spinner mientras espera, es síncrono. Si el usuario sigue navegando y "te avisamos cuando esté listo", es asíncrono.
12. ¿Cómo diseñás un sistema de colas de mensajes?
Un message queue desacopla productores de consumidores. Las componentes:
- Producer: publica mensajes
- Queue/Topic: almacena mensajes temporalmente
- Consumer: procesa mensajes, a su propio ritmo
- Dead letter queue (DLQ): donde van los mensajes que fallaron N veces
[Order Service] → [queue: orders_created] → [Inventory Service]
→ [Email Service]
→ [Analytics Service]Preguntas de seguimiento típicas:
¿Cómo garantizás que un mensaje se procesa exactamente una vez? R: es muy difícil. En la práctica garantizás "at least once" y hacés los consumidores idempotentes (si procesan el mismo mensaje dos veces, el resultado es el mismo).
¿Cómo ordenás mensajes? R: Kafka particiona por key y garantiza orden dentro de una partición. Si necesitás orden global, una sola partición, pero perdés throughput.
13. ¿Kafka vs RabbitMQ: cuándo usás cada uno?
Kafka:
- Event streaming a gran escala (millones de eventos/segundo)
- Retención de mensajes larga (podés "rebobinar" y reprocesar)
- Múltiples consumers independientes (cada uno lleva su propio offset)
- Casos: analytics en tiempo real, event sourcing, pipelines de datos
RabbitMQ:
- Task queues donde el mensaje se elimina al ser procesado
- Routing complejo (exchanges, bindings, topics)
- Casos: background jobs, notificaciones, sistemas de tareas
La respuesta que el entrevistador espera: no "usaría Kafka porque es mejor", sino "para este caso específico donde necesito múltiples consumers independientes que puedan reprocesar eventos, Kafka es la elección correcta. Si solo necesito descargar trabajo a workers, RabbitMQ es más simple."
14. ¿Qué es un circuit breaker y cuándo lo implementás?
Un circuit breaker detecta cuándo un servicio downstream está fallando y deja de llamarlo temporalmente, en lugar de acumular requests que van a fallar igual.
Estados:
- Closed (normal): los requests pasan
- Open (servicio caído): los requests fallan inmediatamente sin llegar al servicio
- Half-open (probando): deja pasar algunos requests para ver si el servicio se recuperó
import time
from enum import Enum
class State(Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=30):
self.state = State.CLOSED
self.failure_count = 0
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.last_failure_time = None
def call(self, func, *args, **kwargs):
if self.state == State.OPEN:
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = State.HALF_OPEN
else:
raise Exception("Circuit breaker OPEN — fast fail")
try:
result = func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise e
def _on_success(self):
self.failure_count = 0
self.state = State.CLOSED
def _on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = State.OPENDiseños clásicos de sistemas
15. Diseñá un URL shortener (tipo bit.ly)
Este es el más común en entrevistas de nivel medio. El entrevistador mide si sabés estimar escala y elegir el almacenamiento correcto.
Estimación:
- 100M URLs nuevas por día = ~1,150 writes/segundo
- 10:1 read/write ratio = ~11,500 reads/segundo
- URL corta: ~7 chars. 100M * 7 bytes = ~700 MB/día de storage
Generación del short code:
import hashlib
import base64
def shorten(long_url: str) -> str:
# MD5 del URL → tomar primeros 6 chars del base64
digest = hashlib.md5(long_url.encode()).digest()
code = base64.urlsafe_b64encode(digest)[:6].decode()
return f"https://short.ly/{code}"
# Problema: colisiones. Mejor: auto-increment + base62 encoding
def to_base62(n: int) -> str:
chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
result = []
while n:
result.append(chars[n % 62])
n //= 62
return "".join(reversed(result)) or "0"Arquitectura:
- Un ID autoincremental en la DB (o un servicio de ID distribuido como Twitter Snowflake)
- Convierte el ID a base62 → ese es el short code
- El redirect: caché (Redis) primero, DB si hay miss → HTTP 301 (permanente) o 302 (temporal para analytics)
Trade-off que mencionás: 301 vs 302. Con 301, el browser cachea el redirect y no volvés a ver esa visita en analytics. Con 302, cada visita pasa por tu servidor — mejor para métricas, peor para latencia.
16. Diseñá Twitter/X (el feed)
La pregunta que más diferencia seniors de midlevels. El core challenge: fanout.
Cuando Lady Gaga (150M followers) postea, ¿cómo hacés que 150 millones de usuarios vean ese tweet en su feed?
Fanout on write (push model):
Post tweet → escribir en los 150M timelines inmediatamentePro: el read del feed es O(1). Con: el write de una celebrity puede durar horas.
Fanout on read (pull model):
Read feed → buscar tweets de cada usuario que seguís → mergearPro: el write es O(1). Con: el read es O(n_following) — costoso para usuarios que siguen a 5,000 personas.
La solución híbrida que usan sistemas reales:
- Para usuarios normales: fanout on write (la mayoría tiene < 5,000 followers, el write es rápido)
- Para cuentas con > N followers (celebrities): fanout on read
- El feed del usuario = precomputado (regular users) + on-demand (celebrities que sigue)
def get_feed(user_id: int, page: int) -> list[Tweet]:
# 1. Tweets precomputados en cache del usuario
cached_tweets = redis.lrange(f"feed:{user_id}", page*20, page*20+19)
# 2. Celebrities que sigue → fetch on-demand
celebrity_ids = get_followed_celebrities(user_id)
celeb_tweets = [get_recent_tweets(cid, limit=20) for cid in celebrity_ids]
# 3. Merge y ordenar por timestamp
all_tweets = cached_tweets + [t for tweets in celeb_tweets for t in tweets]
return sorted(all_tweets, key=lambda t: t.timestamp, reverse=True)[:20]17. Diseñá un sistema de notificaciones push
Componentes:
- 1Notification Service: recibe el evento y lo enruta
- 2User Preference Service: si el usuario tiene notificaciones activadas, qué canal (push, email, SMS)
- 3Template Service: renderiza el mensaje
- 4Delivery Workers: integración con APNs (iOS), FCM (Android), SMTP, Twilio
El punto clave de diseño: rate limiting por usuario. Si un usuario recibe 500 notificaciones por minuto (bug en el sistema), tiene que haber un throttle. También: deduplication — si el mismo evento se publica dos veces (retry de la cola), no querés mandar la notificación dos veces.
class NotificationDeduplicator:
def __init__(self, redis_client, ttl_seconds=3600):
self.redis = redis_client
self.ttl = ttl_seconds
def is_duplicate(self, notification_id: str) -> bool:
key = f"notif:seen:{notification_id}"
# SET key 1 NX EX ttl — atómico en Redis
result = self.redis.set(key, 1, nx=True, ex=self.ttl)
return result is None # None = ya existía = duplicado18. Diseñá un sistema de rate limiting
Un rate limiter protege tu API de abuso y asegura fair use.
Algoritmos principales:
Token Bucket: tenés un bucket con N tokens. Cada request consume 1 token. Los tokens se recargan a tasa fija. Permite bursts hasta el tamaño del bucket.
Sliding Window Log: guardás el timestamp de cada request en una ventana deslizante. Si hay más de N en los últimos X segundos, rechazás. Preciso pero costoso en memoria.
Fixed Window Counter: contador simple por ventana de tiempo (por minuto, por hora). Problema: un usuario puede hacer 2x requests en el boundary de dos ventanas.
# Token Bucket con Redis
import time
import redis
def is_allowed(user_id: str, max_tokens: int, refill_rate: float) -> bool:
r = redis.Redis()
now = time.time()
key = f"ratelimit:{user_id}"
pipe = r.pipeline()
pipe.hgetall(key)
results = pipe.execute()
data = results[0]
if not data:
tokens = max_tokens - 1
last_refill = now
else:
last_refill = float(data[b'last_refill'])
tokens = float(data[b'tokens'])
# Recargar tokens basado en tiempo transcurrido
elapsed = now - last_refill
tokens = min(max_tokens, tokens + elapsed * refill_rate)
last_refill = now
tokens -= 1
if tokens < 0:
return False # Rate limited
pipe = r.pipeline()
pipe.hmset(key, {'tokens': tokens, 'last_refill': last_refill})
pipe.expire(key, 3600)
pipe.execute()
return TrueDistributed rate limiting: con múltiples API servers, cada uno tiene su propio contador local → inconsistencia. Solución: Redis centralizado como fuente de verdad, o algoritmos probabilísticos (cada nodo hace una estimación y rechaza si la estimación supera el límite).
19. Diseñá un sistema de búsqueda con autocompletar
El challenge: para cada keystroke del usuario, responder en < 100ms con sugerencias relevantes.
Estructura de datos: Trie (prefix tree). Cada nodo representa un caracter. El camino desde la raíz hasta un nodo es el prefijo buscado.
class TrieNode:
def __init__(self):
self.children: dict[str, TrieNode] = {}
self.is_end = False
self.frequency = 0 # para rankear por popularidad
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str, frequency: int = 1):
node = self.root
for char in word.lower():
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.frequency = max(node.frequency, frequency)
node.is_end = True
def search_prefix(self, prefix: str, limit: int = 10) -> list[str]:
node = self.root
for char in prefix.lower():
if char not in node.children:
return []
node = node.children[char]
return self._dfs(node, prefix, limit)
def _dfs(self, node: TrieNode, current: str, limit: int) -> list[str]:
results = []
if node.is_end:
results.append((node.frequency, current))
for char, child in node.children.items():
results.extend(self._dfs(child, current + char, limit))
results.sort(reverse=True)
return [word for _, word in results[:limit]]En producción: el Trie en memoria se precalcula offline. Para escala: particionás por primera letra (26 shards), cacheas los prefijos más comunes (top 1M queries), y actualizás el Trie con un batch job diario (no en tiempo real).
20. Diseñá un sistema de video streaming (tipo YouTube)
Las piezas clave:
Upload pipeline:
[Usuario sube video] → [Upload Service] → [Object Storage (S3)]
→ [Queue: video_uploaded]
→ [Transcoding Workers] → [múltiples resoluciones: 360p, 720p, 1080p, 4K]
→ [CDN distribution]Adaptive Bitrate Streaming (ABR): el cliente no pide "el video", pide segmentos de 2-4 segundos. Detecta el bandwidth disponible y pide la resolución apropiada para cada segmento. Protocolo: HLS (Apple) o DASH.
El punto de diseño que diferencia candidatos: el CDN. No servís el video desde tu origin server — lo cacheás en edge nodes alrededor del mundo. La primera vez que alguien en Buenos Aires ve un video popular, el edge node lo descarga del origin y lo cachea. Todos los siguientes lo sirven desde el edge.
Deduplication de videos: si dos usuarios suben el mismo video (contenido idéntico), ¿guardás dos copias? Podés usar un hash del contenido (SHA-256 del archivo) para detectar duplicados. YouTube hace esto para detectar copyrights también.
Consistencia y transacciones
21. ¿Qué es una transacción distribuida y cómo la manejás?
Una transacción distribuida involucra múltiples servicios o bases de datos que deben commitear o rollback juntos.
2-Phase Commit (2PC):
- Fase 1 (Prepare): el coordinador pregunta a todos los participantes si pueden commitear
- Fase 2 (Commit/Abort): si todos dijeron sí, el coordinador manda commit; si alguno dijo no, manda abort
Problema: el coordinador es un SPOF. Si se cae entre la fase 1 y la 2, los participantes quedan bloqueados.
Saga pattern (el que se usa en microservices):
Order Service: crea orden (estado: PENDING)
→ Payment Service: cobra (ok)
→ Inventory Service: descuenta stock (ok)
→ Shipping Service: crea envío (FALLA)
← Compensating transaction: devuelve stock
← Compensating transaction: reembolsa pago
← Order Service: cancela ordenCada paso tiene una transacción compensatoria. No hay rollback atómico, pero eventualmente el sistema llega a un estado consistente.
22. ¿Qué es idempotencia y por qué es crítica?
Una operación es idempotente si ejecutarla una o N veces produce el mismo resultado.
En sistemas distribuidos, los retries son inevitables (timeouts, fallos de red). Si tu operación no es idempotente, un retry puede cobrar dos veces, crear dos órdenes, o enviar dos emails.
# No idempotente - peligroso
def charge_user(user_id: str, amount: float):
db.execute("INSERT INTO charges (user_id, amount) VALUES (?, ?)", user_id, amount)
# Idempotente con idempotency key
def charge_user(user_id: str, amount: float, idempotency_key: str):
existing = db.execute(
"SELECT id FROM charges WHERE idempotency_key = ?",
idempotency_key
).fetchone()
if existing:
return existing['id'] # Ya procesado, devolvé el resultado anterior
return db.execute(
"INSERT INTO charges (user_id, amount, idempotency_key) VALUES (?, ?, ?)",
user_id, amount, idempotency_key
).lastrowidStripe usa esto para todos sus endpoints de pago. El cliente genera un UUID por intento de cobro y lo manda como header Idempotency-Key.
23. ¿Cómo implementás distributed locking?
Cuando múltiples instancias de un servicio intentan acceder al mismo recurso, necesitás un lock distribuido.
Con Redis (Redlock):
import redis
import uuid
import time
class DistributedLock:
def __init__(self, redis_client, key: str, ttl_ms: int = 5000):
self.r = redis_client
self.key = f"lock:{key}"
self.ttl = ttl_ms
self.token = str(uuid.uuid4())
def acquire(self, timeout_ms: int = 1000) -> bool:
deadline = time.time() + timeout_ms / 1000
while time.time() < deadline:
# SET key token NX PX ttl — atómico
acquired = self.r.set(self.key, self.token, nx=True, px=self.ttl)
if acquired:
return True
time.sleep(0.01)
return False
def release(self):
# Solo liberar si el token es el nuestro (evitar liberar el lock de otro)
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
self.r.eval(script, 1, self.key, self.token)Problema del Redlock con un solo nodo: si el nodo Redis se cae justo después de adquirir el lock, el lock desaparece y otro proceso puede adquirirlo antes de que el TTL expire. Para producción seria: usar los 5 nodos Redis del algoritmo Redlock oficial, o mejor aún, ZooKeeper/etcd.
Preguntas de diseño avanzado
24. Diseñá un sistema de geolocalización (tipo Uber)
El challenge central: encontrar todos los drivers dentro de X km del rider en tiempo real.
Naive approach: para cada request de rider, calcular la distancia a todos los drivers. Con 1M drivers activos: inviable.
GeoHash: convierte coordenadas (lat, lng) en un string alfanumérico. Puntos cercanos tienen prefijos similares.
# GeoHash simplificado — en producción usás una librería
import geohash2 # pip install geohash2
def find_nearby_drivers(lat: float, lng: float, radius_km: float) -> list[str]:
# Precision 6 = ~1.2km de precisión
center_hash = geohash2.encode(lat, lng, precision=6)
# GeoHash vecinos (9 celdas: centro + 8 adyacentes)
neighbors = geohash2.neighbors(center_hash) + [center_hash]
# Query Redis con SMEMBERS por cada celda
driver_ids = []
for cell in neighbors:
ids = redis.smembers(f"drivers:geo:{cell}")
driver_ids.extend(ids)
return driver_ids
def update_driver_location(driver_id: str, lat: float, lng: float):
new_hash = geohash2.encode(lat, lng, precision=6)
old_hash = redis.get(f"driver:{driver_id}:geohash")
if old_hash != new_hash:
if old_hash:
redis.srem(f"drivers:geo:{old_hash}", driver_id)
redis.sadd(f"drivers:geo:{new_hash}", driver_id)
redis.set(f"driver:{driver_id}:geohash", new_hash)En producción: Redis tiene GEOADD / GEODIST / GEORADIUS nativos. Pero entender GeoHash demuestra que conocés la fundamentación.
25. Diseñá un sistema de recomendaciones
El challenge: para cada usuario, recomendar ítems que probablemente le gusten sin haberlos visto.
Collaborative Filtering (User-Based):
- Encontrá usuarios similares a ti (mismos likes)
- Recomendá lo que ellos vieron y vos no
Collaborative Filtering (Item-Based):
- Encontrá ítems similares a los que te gustaron
- Netflix lo usa: "porque viste X, te puede gustar Y"
Matrix Factorization (lo que usan los sistemas grandes):
- Descomponés la matriz user×item en dos matrices latentes de menor dimensión
- Cada usuario y cada ítem quedan representados como vectores en ese espacio latente
- La recomendación es el producto punto:
score(user, item) = user_vec · item_vec
El punto de escala que importa en la entrevista: computar recomendaciones en tiempo real para millones de usuarios es imposible. Se precomputan offline (batch job diario/semanal) y se cachean. Para "tiempo real" solo actualizás basado en la última sesión.
26. Diseñá un sistema de búsqueda distribuida (tipo Elasticsearch)
Los componentes:
Inverted Index: la estructura de datos core. Para cada término, guardás la lista de documentos que lo contienen.
"python" → [doc_1, doc_4, doc_7, doc_23]
"interview" → [doc_1, doc_2, doc_11]
"python interview" → intersección → [doc_1]Sharding de índices: particionás el índice entre múltiples nodos. Cada query se manda a todos los shards en paralelo, y los resultados se mergean y re-rankean.
Relevance scoring: TF-IDF (frecuencia del término en el doc × inverso de la frecuencia en el corpus) o BM25.
El punto avanzado: near-real-time indexing. Elasticsearch mantiene un buffer en memoria, lo escribe a segmentos inmutables cada segundo (refresh), y luego los mergea (merge = compactación). Los documentos son buscables ~1 segundo después de ser indexados.
27. Diseñá un sistema de pagos
Es la entrevista con más foco en correctness que en escala.
Propiedades críticas:
- Idempotencia: un pago no puede ejecutarse dos veces
- Atomicidad: el débito y el crédito son una sola operación
- Auditoría completa: cada estado del pago se loguea con timestamp
State machine de un pago:
CREATED → PROCESSING → AUTHORIZED → CAPTURED → SETTLED
↓ ↓
DECLINED REFUNDED
↓
FAILEDNunca actualizás el estado en-place. Cada transición es un INSERT nuevo en la tabla de eventos (event sourcing). El estado actual es el último evento.
Doble-entry bookkeeping (cómo los sistemas reales evitan bugs de balance):
-- Cada transacción genera dos filas (debit + credit)
INSERT INTO ledger (account_id, amount, transaction_id, entry_type)
VALUES
('acc_buyer', -100.00, 'txn_123', 'DEBIT'),
('acc_seller', +100.00, 'txn_123', 'CREDIT');
-- La suma de todos los entries siempre debe ser cero
SELECT SUM(amount) FROM ledger; -- debe ser 028. Diseñá un sistema de logs y monitoreo
Los tres pilares del observability:
Logs: eventos discretos. "El usuario X hizo login a las 14:32:05."
Metrics: series de tiempo numéricas. "CPU al 78%, 1,230 RPS."
Traces: el camino completo de un request a través de múltiples servicios.
Pipeline típico:
[Service] → [Log Agent (Fluentd/Filebeat)] → [Queue (Kafka)]
→ [Stream Processor (Flink/Spark)] → [TimeSeries DB (InfluxDB/Prometheus)]
→ [Search Engine (Elasticsearch)]
→ [Alerting (PagerDuty)]El sampling problem: si logueas cada request a 100,000 RPS, tu pipeline de logs consume más recursos que tu aplicación. Estrategia: sample al 1% para traces normales, 100% para errores, y hot paths configurables.
29. Diseñá un sistema de archivos distribuido (tipo Google Drive)
Split en chunks: los archivos se dividen en bloques de 4-64 MB. Cada bloque se replica en N nodos (típicamente 3).
Ventajas:
- Si un nodo cae, los otros 2 tienen el bloque
- Podés descargar partes del archivo en paralelo desde distintos nodos
- Deduplicación por bloque (dos archivos que comparten el mismo bloque no duplican storage)
Metadata service: un servicio separado que sabe qué bloques forman cada archivo, en qué nodos están, y el orden. Es el directorio. Se replica con alta disponibilidad.
Sync client (el challenge real de Dropbox/Drive):
- Detectar cambios locales:
inotifyen Linux,FSEventsen macOS,ReadDirectoryChangesWen Windows - Delta sync: no re-subís el archivo entero, solo los bloques que cambiaron
- Conflict resolution: dos usuarios editaron el mismo archivo simultáneamente → crea una copia de conflicto (la estrategia más segura)
Preguntas sobre disponibilidad y reliability
30. ¿Cómo diseñás para alta disponibilidad?
Los números concretos que tenés que conocer:
| Disponibilidad | Downtime por año |
|---|---|
| 99% | 3.65 días |
| 99.9% | 8.76 horas |
| 99.99% | 52.6 minutos |
| 99.999% | 5.26 minutos |
Técnicas:
- Redundancia activa: múltiples instancias procesando en paralelo, el fallo de una es transparente
- Redundancia pasiva (standby): una instancia primaria, una de backup que toma el control si la primaria falla (failover)
- Health checks: load balancers que dejan de enviar tráfico a instancias unhealthy
- Multi-region: replica en al menos dos regiones geográficas para sobrevivir a una caída de datacenter
Lo que el entrevistador quiere ver: que entiendas que la disponibilidad tiene un costo. 99.999% requiere infraestructura multi-region, runbooks de failover, chaos engineering regular, y un equipo de oncall. No siempre vale la pena.
31. ¿Qué es backpressure y cómo lo manejás?
Backpressure ocurre cuando el producer genera datos más rápido de lo que el consumer puede procesar.
Producer: 10,000 eventos/segundo
Consumer: puede procesar 3,000 eventos/segundo
Cola: se llena → empieza a dropear eventos o bloquear al producerEstrategias:
- Drop: descartás eventos bajo carga. Aceptable para métricas no críticas.
- Slow down producer: el consumer le señala al producer que frene (protocols como gRPC streaming lo soportan nativo).
- Scale consumers: agregar más instancias de consumer en tiempo real (Kubernetes Horizontal Pod Autoscaler basado en profundidad de la cola).
- Load shedding: rechazás requests cuando el sistema está sobrecargado. Es mejor responder 503 que responder 20 segundos después.
32. Explicá el concepto de "thundering herd" y cómo lo mitigás
Thundering herd: cuando un evento hace que muchos procesos o nodos accedan simultáneamente al mismo recurso.
Ejemplo 1: tu caché expira y 10,000 requests concurrentes van a la DB.
Ejemplo 2: el servidor de DNS se cae, se recupera, y miles de clientes intentan conectarse al mismo tiempo.
Mitigaciones:
- Mutex/semaphore en el caché: solo el primer thread que detecta el miss va a la DB; los demás esperan el resultado (también llamado "request coalescing").
- Probabilistic early expiration: en lugar de expirar exactamente a tiempo, empezás a refrescar el caché antes de que expire, con probabilidad creciente. Nunca todo expira junto.
- Jitter en los retries: en lugar de reintentar exactamente cada N segundos, agregás aleatoriedad.
sleep(base * 2^attempt + random(0, base)).
33. ¿Cómo diseñás un sistema de health checks y alertas?
Liveness check: ¿el proceso sigue vivo? Un endpoint /health que responde 200 OK. Kubernetes lo usa para decidir si reiniciar el pod.
Readiness check: ¿el servicio está listo para recibir tráfico? Verifica dependencias (DB conectada, caché disponible). El load balancer lo usa para decidir si enviar tráfico.
from fastapi import FastAPI
import redis
import psycopg2
app = FastAPI()
@app.get("/health/live")
def liveness():
return {"status": "ok"}
@app.get("/health/ready")
def readiness():
checks = {}
try:
redis.Redis().ping()
checks["redis"] = "ok"
except Exception as e:
checks["redis"] = f"error: {e}"
try:
conn = psycopg2.connect(DATABASE_URL)
conn.close()
checks["db"] = "ok"
except Exception as e:
checks["db"] = f"error: {e}"
all_ok = all(v == "ok" for v in checks.values())
return {"status": "ok" if all_ok else "degraded", "checks": checks}Alerting: la señal más confiable no son los logs, son las métricas de negocio. Si las órdenes por minuto caen 50% en 5 minutos, algo está roto aunque todos los servicios reporten "healthy".
Preguntas sobre seguridad y compliance
34. ¿Cómo diseñás autenticación y autorización en microservices?
Autenticación (quién sos): JWT tokens emitidos por un Auth Service central. Cada servicio valida el JWT localmente sin llamar al Auth Service en cada request.
import jwt
from datetime import datetime, timedelta
SECRET = "your-256-bit-secret"
def create_token(user_id: str, roles: list[str]) -> str:
payload = {
"sub": user_id,
"roles": roles,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1)
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def verify_token(token: str) -> dict:
return jwt.decode(token, SECRET, algorithms=["HS256"])
# Lanza jwt.ExpiredSignatureError si expiró
# Lanza jwt.InvalidTokenError si fue manipuladoAutorización (qué podés hacer): RBAC (Role-Based) es el standard. Cada endpoint verifica si el rol del usuario incluye el permiso necesario.
El punto de madurez en la entrevista: mencionar que los JWTs tienen una limitación — no podés revocarlos antes de que expiren (son stateless). Solución: access tokens de vida corta (15 min) + refresh tokens en DB (revocables). O un token blacklist en Redis para revocación urgente.
35. ¿Cómo protegés datos sensibles en un sistema distribuido?
Encryption at rest: los datos en disco están encriptados. AWS, GCP, Azure lo hacen transparent con sus storage services. Para PII especialmente sensible: field-level encryption (encriptás el campo antes de persistir, la DB no ve el plaintext).
Encryption in transit: TLS 1.3 entre todos los servicios. En microservices: mutual TLS (mTLS) donde ambas partes se autentican, no solo el cliente.
Tokenization: reemplazás el dato sensible (número de tarjeta) con un token random. El token vive en tus sistemas; el dato real vive solo en un PCI-DSS compliant vault. Stripe hace esto — vos solo guardás el pm_xxx token.
Data minimization: no guardés datos que no necesitás. Si el usuario te dio su dirección para hacer el envío y ya se envió, ¿necesitás seguir guardándola?
Preguntas de estimación (Fermi)
36. ¿Cuánto storage necesita WhatsApp para los mensajes de un día?
Este tipo de pregunta mide si podés pensar en órdenes de magnitud sin calculadora.
Proceso:
- WhatsApp: ~100B usuarios activos, ~65B mensajes/día
- Tamaño promedio de mensaje de texto: ~100 bytes
- 65B mensajes × 100 bytes = 6.5 TB de texto/día
- ~15% de mensajes son media (fotos, videos, audio)
- Foto promedio comprimida: ~100 KB; 10B mensajes media × 100 KB = ~1 PB/día
- Total: ~1 PB/día de storage nuevo (dominado por media, no texto)
Lo que el entrevistador evalúa: no el número exacto, sino el proceso. Descomponés el problema, estimás cada componente, identificás qué domina.
37. ¿Cuántos servidores necesita Google Search?
- Búsquedas globales: ~8.5B/día ≈ ~100,000 búsquedas/segundo
- Una búsqueda involucra: crawling (datos ya indexados), ranking (CPU intensivo), serving (respuesta)
- Un servidor moderno puede manejar ~1,000-5,000 requests/segundo de serving
- Solo para serving: 100,000 / 2,000 = ~50 servidores (parecería poco pero recordá CDN + caché de resultados populares)
- Index servers, crawl servers, ranking servers: multiplicate por 10-100x
- Con replicación, geografía, spare capacity: ~1M servidores es razonable (Google publicó ~2M en 2020)
Conceptos que siempre caen al final
38. ¿Qué es un vector clock y cuándo lo usás?
Un vector clock es un mecanismo para rastrear causalidad en sistemas distribuidos sin sincronizar relojes físicos.
Nodo A: [A:1, B:0, C:0] ← A hace un evento
Nodo B: [A:1, B:1, C:0] ← B recibe el evento de A y hace uno propio
Nodo C: [A:1, B:1, C:1] ← C recibe de B y hace uno propioSi dos eventos tienen vector clocks que no son comparables, hay un conflicto (ocurrieron concurrentemente). Amazon DynamoDB usó vector clocks para detectar conflictos y presentarle al cliente las versiones en conflicto.
39. ¿Qué es el problema del Two Generals?
Es la prueba de que no existe ningún protocolo que garantice acuerdo entre dos partes sobre una acción futura a través de un canal no confiable.
Relevancia práctica: muestra por qué los ACKs/ACKs-de-ACKs no terminan nunca — siempre hay incertidumbre. En la práctica lo resolvemos con timeouts + idempotencia + retries, aceptando que el sistema puede estar en estados inconsistentes brevemente.
40. ¿Qué es CRDTs y cuándo los usás?
CRDT (Conflict-free Replicated Data Type): estructuras de datos diseñadas para que múltiples réplicas puedan actualizarse independientemente y mergearse sin conflictos.
Ejemplo: G-Counter (Grow-only Counter). Cada nodo mantiene su propio contador. El valor global es la suma de todos. No podés decrementar (por eso "grow-only"), pero podés tener N réplicas incrementando sin coordinación y el merge es trivial.
Usado en: Figma (colaboración en tiempo real), Redis CRDT (módulo enterprise), Riak.
La limitación: no todos los tipos de datos tienen un CRDT natural. Para operaciones que requieren semántica de "último gana" o "primer gana", seguís necesitando coordinación.
Cómo usar este material en tu entrevista
Memorizar respuestas es la estrategia incorrecta. Lo que funciona:
1. Practicá el framework de clarificación: antes de diseñar nada, hacé 3-5 preguntas que demuestren que pensás en el problema de negocio, no solo en el técnico.
2. Hablá los trade-offs en voz alta: "Podría usar X, que tiene la ventaja de A y B, pero el costo de C. Dada la escala que mencionaste, voy con Y porque..." Eso es lo que separa senior de midlevel.
3. Usá números concretos: "Asumiendo 1M usuarios activos con 10 requests promedio por día, son ~116 RPS, lo cual un solo servidor maneja con comodidad. A 100M usuarios, necesito escala horizontal."
4. Identificá el componente más interesante: en cada diseño hay un cuello de botella o un trade-off que domina todos los demás. Encontralo y profundizá ahí — el entrevistador lo va a hacer de todas formas.
5. Usá InterviewHack.ai para practicar con preguntas adaptadas a la empresa real donde aplicás — investigamos quién te va a entrevistar y qué sistemas usan, para que tu práctica sea específica, no genérica.
Recursos para profundizar
- "Designing Data-Intensive Applications" (Martin Kleppmann) — el libro que todos los que entrevistan para Staff+ leyeron
- System Design Primer (GitHub) — gratuito, cubre los patrones esenciales
- AWS/Google Cloud whitepapers — lee cómo los sistemas reales resolvieron los problemas que estudiaste
- Engineering blogs: Stripe, Cloudflare, Discord, Figma — todos publican posts técnicos de sus decisiones de arquitectura