Una capa pequeña y reutilizable que vuelve confiables los webhooks de terceros: nada se pierde, nada se procesa dos veces, todo se puede reproducir.
Rol
Arquitectura y diseño
Período
2026
Empresa
Proyecto personal
Persist-first
guardar el evento crudo, luego responder 200
Dedupe
por ID de evento del proveedor, forzado por clave única
Replay
cualquier evento o rango de tiempo, bajo demanda
#Contexto
Los proveedores de pagos, mensajería y telefonía entregan cambios de estado por webhooks, y todos se comportan igual: reintentan, entregan fuera de orden, envían duplicados, y dan timeout si el handler es lento. Las integraciones que hacen el trabajo dentro del handler HTTP terminan perdiendo o aplicando dos veces algún evento. Este kit aísla ese problema una sola vez para que cada integración nueva solo escriba su handler.
#Problema y restricciones
Procesar dentro del request significa que una llamada lenta aguas abajo se vuelve un timeout del proveedor y una tormenta de reintentos.
Los proveedores garantizan entrega al-menos-una-vez, así que los handlers ven duplicados salvo que algo los elimine.
Cuando sale un bug en un handler, los eventos afectados se pierden salvo que se hayan guardado los payloads crudos.
#Arquitectura
Los eventos se verifican y guardan antes del 200; el procesamiento, los reintentos y el replay ocurren detrás del inbox.
El endpoint HTTP hace tres cosas y nada más: verificar la firma del proveedor contra el body crudo, insertar el evento crudo en una tabla inbox de PostgreSQL con clave por proveedor e ID de evento, y responder 200. La clave única convierte una entrega duplicada en un no-op. Un relay toma las filas nuevas del inbox y las encola en BullMQ; los workers ejecutan el handler específico de la integración con backoff exponencial y jitter. Un job que agota sus intentos pasa a una cola dead-letter con el error adjunto, y la fila del inbox registra el estado de cada intento. Como se guardan el payload crudo y los headers, un CLI puede reproducir un solo evento, todo lo que está en la cola dead-letter, o un rango de tiempo — que es también como un handler corregido se aplica a los eventos en los que antes falló. Los handlers se escriben idempotentes, con el ID de evento disponible como clave natural.
#Decisiones clave y tradeoffs
01Inbox en Postgres antes de la cola
Decisión
Escribir el evento primero en una tabla durable y encolar desde ahí, en vez de empujar directo a Redis.
Alternativas
Encolar directamente desde el handler HTTP, o procesar de forma síncrona.
Por qué
El inbox es el sistema de registro: sobrevive a una caída de la cola, da deduplicación con una simple restricción única, y hace del replay una consulta.
Costo de equivocarse
Una escritura extra por evento y un proceso relay que operar; la tabla necesita una política de retención.
02Cola dead-letter con replay
Decisión
Apartar los eventos fallidos con su error tras reintentos acotados, y hacer que volver a ejecutarlos sea un comando de primera clase.
Alternativas
Reintentar para siempre, o descartar tras el último intento y depender de logs.
Por qué
Los reintentos infinitos esconden bugs reales y bloquean la cola; descartar pierde datos. Un estacionamiento visible y reproducible convierte un incidente en un fix más un comando.
Costo de equivocarse
Alguien tiene que vigilar la cola dead-letter; sin una alerta se vuelve un backlog silencioso.
#Resultados
At-least-once
entrega convertida en procesamiento efectivamente-una-vez
< 1 handler
de trabajo en el camino del request: verificar, insertar, confirmar
1 command
para reproducir un evento, la DLQ o un rango de tiempo
#Qué haría diferente
Lo que este diseño no resuelve es el orden: dos eventos de la misma entidad pueden procesarse fuera de secuencia. El siguiente paso son claves de orden por entidad en la cola y una prueba de caos — workers caídos, entregas duplicadas y reordenadas — para ponerle números al throughput y confirmar que nada se pierde.