Ronda técnica · identificar errores en un diseño

Diez diseños con un error

Alex dijo que la ronda de system design consiste en identificar errores. Eso no es diseñar de cero: es leer un sistema ajeno y ver lo que está mal. Es la prueba más preparable de todas. Doce casos, del más común al más sutil, con dos específicos de Node.

MÉTODOCómo se revisa un diseño

No empieces por los detalles. Recorre siempre estas seis capas, en orden, y di en voz alta cuál estás mirando:

#CapaPregunta que haces
1Entrada¿Qué llega, validado, y qué pasa si llega dos veces?
2Volumen¿Qué se rompe a 10×? ¿hay tope en algún sitio?
3Estado¿Quién es la verdad? ¿qué pasa si dos escrituras coinciden?
4Consultas¿cuántas por pantalla? ¿con qué índice? ¿cuánto devuelven?
5Fallos¿reintentos? ¿timeouts? ¿qué se pierde y qué se duplica?
6Operación¿cómo sabes que está roto antes que el usuario?

CASOSDoce diseños para romper

01El listado que se cae con 200 filas
for (const pedido of pedidos) {
  const cliente = await db.cliente.findById(pedido.clienteId)
  resultados.push({ ...pedido, cliente })
}
Ver el error
Qué está mal
N+1: una consulta por fila. Con 200 pedidos son 201 viajes a la base.
Arreglo
Una sola consulta con IN sobre los ids, o un JOIN. Se reduce a 2.
Cómo lo detectas
El número de queries crece con el número de filas. En logs, la misma sentencia repetida N veces.
02El webhook que cobra dos veces
app.post('/webhook/pago', async (req, res) => {
  await cobrar(req.body.pedidoId)
  res.send('ok')
})
Ver el error
Qué está mal
Sin idempotencia y sin verificar firma. Los webhooks se reintentan; el segundo cobra otra vez.
Arreglo
Validar firma, guardar event_id con restricción única y procesar una sola vez. Responder 2xx rápido y encolar.
Cómo lo detectas
Duplicados en la tabla de pagos con el mismo identificador externo.
03El contador que se pierde
const fila = await db.stock.findOne({ id })
if (fila.cantidad >= n) {
  fila.cantidad = fila.cantidad - n
  await db.stock.save(fila)
}
Ver el error
Qué está mal
Condición de carrera: dos peticiones leen el mismo valor y ambas descuentan. Se vende lo que no hay.
Arreglo
Un solo UPDATE ... WHERE cantidad >= n y comprobar filas afectadas, o bloqueo de fila / transacción con nivel adecuado.
Cómo lo detectas
Stock negativo o pérdida de actualizaciones bajo carga concurrente.
04La cola que nunca para de crecer

Un servicio acepta eventos y los mete en una cola en memoria. Un worker los consume. En un pico, la cola crece a 2 millones de mensajes y el proceso muere.

Ver el error
Qué está mal
Cola en memoria (se pierde al reiniciar) y sin contrapresión: acepta más de lo que puede procesar.
Arreglo
Cola durable, tope de tamaño, y política explícita al llenarse: 429 con Retry-After, descartar lo de menor valor, o escalar consumidores.
Cómo lo detectas
Tamaño de cola como métrica de alarma, no el uso de CPU.
05El reintento que tumba al vecino
async function llamar() {
  for (let i = 0; i < 5; i++) {
    try { return await fetch(url) } catch (e) { /* reintenta ya */ }
  }
}
Ver el error
Qué está mal
Reintentos inmediatos, sin espera ni límite global: tormenta de reintentos. Si el downstream está caído, lo rematas.
Arreglo
Backoff exponencial con jitter, tope de intentos, y respetar Retry-After. Cortacircuitos si falla seguido.
Cómo lo detectas
El pico de peticiones coincide con el inicio de la caída del proveedor.
06El pico de las 3 de la mañana

Diez mil claves de caché se guardan con TTL de 24 horas al desplegar. Al día siguiente, a la misma hora, el sistema se cae.

Ver el error
Qué está mal
Cache stampede: todas expiran a la vez y todas las peticiones pegan a la base al mismo tiempo.
Arreglo
TTL con jitter (24 h ± 10 %), revalidación en segundo plano, y un candado para que solo una petición recompute.
Cómo lo detectas
Caída de la tasa de aciertos de caché seguida de un pico de consultas.
07La página que repite y salta filas
SELECT * FROM eventos ORDER BY creado_en DESC LIMIT 50 OFFSET 5000;
Ver el error
Qué está mal
Paginación por offset sobre una tabla que recibe filas nuevas: el usuario ve duplicados y se salta elementos. Además el offset profundo es O(n).
Arreglo
Paginación por cursor sobre (creado_en, id), que es estable y no recorre lo anterior.
Cómo lo detectas
El mismo registro aparece en dos páginas; consultas lentas a partir de la página 50.
08El saldo con centavos fantasma
total = total + 0.1 + 0.2   // esperado 0.3
Ver el error
Qué está mal
Dinero en punto flotante. 0.1 + 0.2 = 0.30000000000000004.
Arreglo
Enteros en la unidad mínima (centavos) o un tipo decimal. Redondeo explícito en la frontera.
Cómo lo detectas
Diferencias de un centavo al cuadrar; sumas que no cierran al recalcular.
09El panel que mata la base

Un dashboard pide "últimos 30 días" y ejecuta un GROUP BY sobre la tabla cruda, que tiene 40 millones de filas. Cada usuario que abre el panel lanza la consulta.

Ver el error
Qué está mal
Agregación al leer sobre datos crudos, sin caché ni pre-cálculo. El coste lo paga cada apertura.
Arreglo
Agregar al escribir: cubetas por minuto/hora/día. La consulta pasa de cálculo a lectura. Y resolución proporcional a la ventana.
Cómo lo detectas
Consultas lentas correlacionadas con la hora de entrada al trabajo, no con el volumen de datos.
10La fecha que miente dos veces al año
SELECT date(fecha) AS dia, count(*) FROM eventos GROUP BY 1;
Ver el error
Qué está mal
Sin zona horaria explícita. date() trunca en la zona del servidor; cambia con el horario de verano y agrupa mal.
Arreglo
Guardar en UTC, truncar con la zona del usuario explícita, y fijar la zona en la consulta.
Cómo lo detectas
Un día con 23 horas y otro con 25; picos a medianoche.
11El endpoint que congela todo el servicio
app.get('/reporte', async (req, res) => {
  const csv = fs.readFileSync('/datos/export.csv')      // sincrónico
  const hash = crypto.pbkdf2Sync(req.query.clave, 'sal', 600000, 64, 'sha512')
  res.send(procesar(csv, hash))
})
Ver el error
Qué está mal
En Node, readFileSync y pbkdf2Sync bloquean el hilo único. Mientras uno corre, el proceso no atiende ninguna otra petición: no es lento, es de una en una.
Arreglo
Versiones asíncronas (fs.promises, crypto.pbkdf2) o worker_threads para CPU intensiva. Nunca *Sync en una ruta.
Cómo lo detectas
El lag del event loop sube y la latencia se dispara para todos, no solo para quien llamó.
12El despliegue que corta peticiones a la mitad

El servicio corre en un contenedor. Al desplegar, el orquestador manda SIGTERM y el proceso muere. Los usuarios ven errores 502 durante el despliegue y algunas peticiones se pierden sin dejar rastro.

Ver el error
Qué está mal
No hay apagado ordenado: deja de aceptar conexiones, espera a las peticiones en vuelo, cierra el pool de base de datos y las colas. Y no hay readiness que lo saque del balanceador antes.
Arreglo
Manejar SIGTERM: marcar not ready, esperar un margen, drenar, cerrar recursos. server.close() y un timeout máximo.
Cómo lo detectas
Errores 502 que solo aparecen en ventanas de despliegue; trabajos a medias sin estado final.

CIERRELas cinco preguntas que siempre puedes hacer

Si te dan un diseño y no ves el error de inmediato, estas cinco preguntas lo encuentran casi siempre. Hazlas en voz alta: el entrevistador está evaluando cómo piensas, no si adivinas.

1. ¿Qué pasa si esto llega dos veces?
2. ¿Qué pasa si esto crece diez veces?
3. ¿Dónde está el límite — el tope, el timeout, el tamaño máximo?
4. ¿Quién es la fuente de la verdad si dos escrituras coinciden?
5. ¿Cómo me entero de que esto está fallando antes que el usuario?

Y la frase que conviene decir al empezar: "Antes de opinar, voy a recorrer seis capas: entrada, volumen, estado, consultas, fallos y operación. Dime si prefieres que empiece por alguna." Eso te compra treinta segundos y ordena la conversación.