Caso de arquitectura · BBVA México
Cómo pasé de envíos síncronos y de «mejor esfuerzo» a un sistema distribuido con patrón Outbox, colas de prioridad y self-healing.
Antes: cada módulo llama directo al proveedor y un 429 hace que el mensaje se pierda. Ahora: el mensaje se guarda, se despacha con control de flujo y se confirma como entregado. Activa el sonido: un tono positivo marca el camino correcto y uno negativo, el incorrecto.
La necesidad
Varios módulos de negocio dependen de notificaciones por correo para avisar a los usuarios. Ese aviso tiene que llegar y, cuando no llega, el equipo necesita saber qué pasó con él en lugar de enterarse por una queja.
El problema
Esa queja se repetía. Al revisar el sistema encontré la causa raíz: el envío era síncrono y de mejor esfuerzo. Si el proveedor respondía 429 (límite de cuota) o un error 5xx, la notificación se perdía sin dejar rastro y sin reintento.
Además, el código pertenecía a otro equipo y estaba poco documentado, así que proponer un cambio de fondo no era sencillo.
Cada módulo llamaba directo al proveedor. Un 429 significaba un mensaje perdido.
Cada mensaje se guarda antes de enviarse y termina entregado o registrado.
Cómo lo solucioné
Diagnóstico y propuesta
Documenté la causa raíz y pedí feedback a personas cuyo criterio admiro. Hubo un momento en que pensé en dejarlo en un parche; sus consejos me animaron a diseñar una propuesta completa y enviársela a mi líder, que la aprobó.
La arquitectura
Los módulos llaman a una fachada única de envío, que filtra el entorno y prevalida dominios inválidos antes de tocar la red.
Todo mensaje se persiste en PENDING antes de intentar enviarse. Si el proceso muere, no se pierde nada.
El cliente sale hacia el API Gateway con rate limiter (token bucket) y exponential backoff a nivel transporte.
Con 202 Accepted y correlation ID, el mensaje pasa a QUEUED y se cierra por webhook en SENT o BOUNCED. Los errores permanentes (422) van a dead letter sin reintento; los recuperables (429, 5xx) pasan a cuatro colas de prioridad con backoff exponencial: crítica, alta, normal y baja.
Un cron de reconciliación rescata mensajes huérfanos en PENDING o en QUEUED sin respuesta de webhook. El reproceso de dead letters reencola por lotes con un delay por índice (staggering) para evitar el thundering herd sobre el proveedor.
La ejecución
La implementación se coordinó con revisiones de código y arquitectura a distancia entre México y España, con pocas horas de horario en común, en un sprint de 8 semanas en paralelo a otro proyecto.
El resultado
Donde ya corre, no hay pérdidas silenciosas: cada mensaje termina entregado o registrado.
Hoy ya opera en parte de los módulos; la migración del resto sigue en curso.
El diagrama y la animación los hice con ayuda de Claude (IA); la arquitectura y el sistema, con mi equipo.