M8 — Casos Reales de Producción: Identificación, Respuestas y Trade-offs
Outcome: Aprender a identificar problemas críticos de producción y responder con propiedad técnica estructurando las soluciones en 3 niveles: Solución Simple (Junior) vs Mínima Viable (Mid) vs Óptima (Senior/Staff).
Caso 1: Pagos y Transacciones Duplicadas (Concurrencia & Webhooks)
El Escenario Real:
Un usuario presiona dos veces el botón “Pagar $100 USD” debido a una conexión móvil lenta, o la pasarela de pagos (Stripe, Wompi, MercadoPago) reintenta el webhook 3 veces tras un micro-corte de red.
1. Solución Simple (Nivel Junior / Anti-patrón)
- Implementación: Insertar el registro en la base de datos cada vez que entra el request HTTP.
- Por qué falla en producción: Se cobra dos o tres veces al usuario. Si el webhook reintenta por timeout, se duplica el saldo. Pérdida directa de dinero y disputas bancarias (chargebacks).
2. Solución Mínima Viable (Nivel Mid)
- Implementación: Enviar un header
Idempotency-Key(UUID) y verificar en Redis:if await redis.get(f"idem:{key}"): return {"status": "already_processed"} - Por qué es incompleta: Vulnerable a TOCTOU (Time-Of-Check to Time-Of-Use). Si dos requests concurrentes llegan en la misma ventana de milisegundos, ambos leen que la clave no existe antes de que cualquiera de los dos la guarde, ambos pasan y cobran doble.
3. Solución Óptima Senior (Estándar de Producción)
- Implementación:
- Lock Atómico en Redis:
# NX = Set if Not eXists, EX = TTL 120s acquired = await redis.set(f"idem:{key}", "PROCESSING", nx=True, ex=120) if not acquired: # Si ya está procesando o terminó, devolver respuesta cacheada cached = await redis.get(f"idem:{key}:response") return json.loads(cached) if cached else {"status": "processing_retry_later"} - Backstop Atómico en PostgreSQL:
Restricción
UNIQUE (idempotency_key)en la tablapayments. Si Redis se reinicia o falla, la base de datos rechaza la segunda inserción a nivel de motor. - Payload Caching:
Al finalizar la transacción, se guarda el body y status code exactos en Redis (
TTL 24h) y se retorna con headerX-Idempotency-Replayed: trueen llamadas subsecuentes.
- Lock Atómico en Redis:
Caso 2: Exportaciones Masivas / Reportes que Agotan la Memoria RAM
El Escenario Real:
El cliente solicita un botón: “Descargar histórico de 500,000 ventas en Excel/CSV”. Cuando 3 administradores lo ejecutan simultáneamente, el contenedor de Docker supera el límite de RAM (OOM Kill) y la aplicación completa se reinicia.
1. Solución Simple (Nivel Junior / Anti-patrón)
- Implementación:
orders = session.scalars(select(Order)).all(), cargar los 500k objetos a memoria RAM, armar un DataFrame con Pandas y devolverlo en la respuesta HTTP directa. - Por qué falla en producción: 500k objetos ORM consumen ~2GB de RAM por usuario. En Kubernetes/Cloud Run, el pod supera su cuota de memoria y muere (CrashLoopBackOff).
2. Solución Mínima Viable (Nivel Mid)
- Implementación: Usar paginación con
LIMIT 1000 OFFSET Xen un bucle dentro del request síncrono para ir escribiendo a un archivo local temporal. - Por qué es incompleta: En páginas altas (
OFFSET 400000), la base de datos debe escanear y descartar 400k filas en cada iteración. La consulta tarda 3 minutos y el balanceador (ALB/Cloudflare) corta la conexión con error504 Gateway Timeout.
3. Solución Óptima Senior (Estándar de Producción)
- Implementación:
- Desacople Asíncrono: El endpoint HTTP valida permisos y encola la tarea, respondiendo de inmediato:
202 Accepted { "job_id": "job_123", "status": "queued" }. - Worker Dedicado (Celery / Background Worker): Un worker independiente toma la tarea y ejecuta un Server-Side Cursor / Keyset Pagination (
WHERE id > last_id ORDER BY id LIMIT 5000) o un stream directo con SQLAlchemy Core (sin instanciar objetos ORM). - Streaming a Storage S3/GCS: El archivo se comprime en formato
.parqueto.csv.gzy se sube por partes (multipart upload) directamente al bucket de almacenamiento sin saturar el disco local. - Notificación: Al finalizar, el worker genera una URL pre-firmada con expiración de 1 hora y notifica al usuario vía correo o WebSocket.
- Desacople Asíncrono: El endpoint HTTP valida permisos y encola la tarea, respondiendo de inmediato:
Caso 3: Endpoints Lentos por Crecimiento de Tablas en PostgreSQL
El Escenario Real:
La tabla orders creció de 10,000 a 4 millones de registros. El endpoint /orders?status=pending pasó de responder en 40ms a tardar 9 segundos, colapsando el pool de conexiones de la base de datos.
1. Solución Simple (Nivel Junior / Anti-patrón)
- Implementación: Aumentar el tamaño de la máquina en AWS RDS (escalamiento vertical) de
db.t3.mediumadb.r5.2xlarge. - Por qué falla en producción: Multiplica el costo mensual por 5 y solo aplaza el problema. Al llegar a 10M de filas volverá a degradarse.
2. Solución Mínima Viable (Nivel Mid)
- Implementación: Agregar un índice B-Tree estándar a la columna:
CREATE INDEX idx_orders_status ON orders(status). - Por qué es incompleta: La columna
statustiene muy baja cardinalidad (ej: 85%completed, 10%pending, 5%cancelled). Si el 10% de 4M filas son 400k filas, PostgreSQL ignorará el índice y ejecutará un Sequential Scan porque el costo de saltos aleatorios en disco (Random I/O) supera la lectura secuencial de la tabla.
3. Solución Óptima Senior (Estándar de Producción)
- Implementación:
- Diagnóstico con
EXPLAIN (ANALYZE, BUFFERS): Identificar si el tiempo se pierde en lectura de disco (shared read) o descarte en CPU (Rows Removed by Filter). - Partial Index (Índice Parcial): Indexar únicamente las órdenes pendientes:
El índice pesa menos de 5MB en RAM en lugar de 400MB y el planner salta directamente en $O(\log N)$.CREATE INDEX idx_orders_pending ON orders(created_at DESC) WHERE status = 'pending'; - Covering Index (
INCLUDE): Si el endpoint necesitaid,totalycreated_at, se agregan conINCLUDE (total)para lograr un Index-Only Scan, eliminando la necesidad de leer las páginas de la tabla en disco (Heap Fetch). - Creación segura sin locks:
Evita que el lock de tipoCREATE INDEX CONCURRENTLY idx_orders_pending ...;SHAREbloquee los inserts y updates concurrentes en producción.
- Diagnóstico con
Caso 4: Fallos en Cascada por Servicios Downstream Degradados
El Escenario Real:
El microservicio de pagos o la API externa de facturación empieza a responder con 25 segundos de latencia. En pocos minutos, tu backend principal agota sus hilos y sockets HTTP, dejando de responder a todos los demás clientes (Cascading Failure).
1. Solución Simple (Nivel Junior / Anti-patrón)
- Implementación: Aumentar el timeout del cliente HTTP a 60 segundos para “darle tiempo a que responda”.
- Por qué falla en producción: Empeora el problema. Cada request entrante retiene un worker durante 60 segundos. En menos de 1 minuto se agotan los 100 workers de la app y el servidor colapsa por completo.
2. Solución Mínima Viable (Nivel Mid)
- Implementación: Configurar un timeout estricto de 3 segundos y agregar un reintento automático simple (
retry 3 times). - Por qué es incompleta: Genera un efecto Thundering Herd. Si el servicio downstream está sobrecargado, triplicar el tráfico con reintentos inmediatos garantiza que nunca se recupere.
3. Solución Óptima Senior (Estándar de Producción)
- Implementación:
- Circuit Breaker:
- Si la tasa de fallos supera el 50% en una ventana de 10s, el circuito pasa a estado
OPEN. - Todos los requests fallan inmediatamente (Fast-Fail) sin saturar la red ni esperar timeouts.
- Si la tasa de fallos supera el 50% en una ventana de 10s, el circuito pasa a estado
- Exponential Backoff con Full Jitter: $$\text{Delay} = \text{random}(0, \min(\text{MaxDelay}, \text{BaseDelay} \cdot 2^{\text{attempt}}))$$ Distribuye los reintentos aleatoriamente en el tiempo para no saturar al servicio en recuperación.
- Graceful Degradation (Degradación Elegante):
Si el circuito está abierto en un servicio secundario (ej. recomendaciones, notificaciones), el endpoint devuelve una lista vacía o un valor por defecto en lugar de un error
500.
- Circuit Breaker:
Caso 5: Concurrencia en Inventarios / Saldos (“Lost Update”)
El Escenario Real:
Queda 1 sola unidad disponible de un producto en descuento. Dos usuarios hacen clic en “Comprar” en el mismo milisegundo. Ambos pedidos se procesan y el inventario queda con sobreventa (inventario negativo).
1. Solución Simple (Nivel Junior / Anti-patrón)
- Implementación:
product = db.query(Product).get(product_id) if product.stock >= 1: product.stock -= 1 db.commit() - Por qué falla en producción: Condición de carrera clásica. La transacción A y la transacción B leen
stock = 1simultáneamente. Ambas calculan1 - 1 = 0y ambas escribenstock = 0. Se vendieron 2 unidades teniendo solo 1.
2. Solución Mínima Viable (Nivel Mid)
- Implementación: Bloqueo pesimista en base de datos:
product = session.scalars( select(Product).where(Product.id == product_id).with_for_update() ).one() - Por qué es aceptable pero con límites: Funciona correctamente a nivel de datos (
RowExclusiveLock), pero si el tráfico es masivo (ej. Black Friday con 10,000 usuarios sobre el mismo producto), la cola de transacciones bloqueadas agota el pool de conexiones de PostgreSQL.
3. Solución Óptima Senior (Estándar de Producción)
- Implementación:
- Atomic Set-Based Update en Base de Datos (Sin locks de lectura):
La cláusulaUPDATE products SET stock = stock - 1 WHERE id = :product_id AND stock >= 1;WHERE stock >= 1es evaluada atómicamente por el motor de la base de datos en el momento exacto del update. - Verificación de Filas Afectadas (
rowcount):
Cero locks sostenidos en la aplicación, máxima concurrencia y garantía 100% libre de sobreventa.result = await session.execute(stmt) if result.rowcount == 0: raise OutOfStockError("El producto ya no cuenta con stock disponible.")
- Atomic Set-Based Update en Base de Datos (Sin locks de lectura):
Cómo Verbalizar estos Casos en Entrevistas y Revisiones Técnicas
- Empieza por el riesgo de negocio: “El riesgo en este escenario es la sobreventa/duplicidad de cobros/caída por memoria.”
- Descarta la solución ingenua: “Hacerlo síncrono o con un check simple no escala porque genera condiciones de carrera (TOCTOU) o derrames de memoria.”
- Presenta la solución en capas: “La arquitectura correcta combina un lock atómico/stream asíncrono en la capa rápida con una restricción a nivel de base de datos como garantía final.”