- Agile 2
- Alta disponibilidad 1
- Alternativas cloud 1
- Aop 1
- Arquitectura 3
- Arquitectura distribuida 2
- Automatizacion 3
- Azure devops 1
- Base de datos 1
- Buenas practicas 19
- Cloud 1
- Colas 7
- Competing consumers 1
- Convenciones 11
- Copilot 1
- Diseno 6
- Docker 2
- Docker compose 1
- Documentacion 1
- Eda 11
- Equipos 1
- Escalabilidad 1
- Flujo de negocio 1
- Flujo de trabajo 3
- Flyway 1
- Git 4
- Gradle 3
- Herramientas digitales 1
- Ia 1
- Iam 1
- Infraestructura 2
- Java 14
- Jerarquia tecnica 1
- Jpa 1
- Jsonb 1
- Kafka 7
- Kubernetes 1
- Liderazgo en software 1
- Lineamientos 1
- Log 1
- Logging 3
- Microservicios 3
- Mongodb 1
- Monitoreo 1
- Nosql 3
- Observabilidad 4
- Open source 1
- Plugins 3
- Postgresql 1
- Privacidad 1
- Programacion funcional 1
- Programacion reactiva 4
- Rabbitmq 6
- Rotacion de talento 1
- Saga 2
- Scrum 2
- Security 1
- Seguridad 1
- Self hosting 1
- Sistemas legados 1
- Spring boot 3
- Spring mvc 2
- Sql 3
- Streams 1
- Threadlocal 1
- Trazabilidad 2
- Versionado 2
- Web 1
- Webflux 2
- Websockets 1
- Zero trust 1
Arquitectura distribuida
2 artículos
Estándar de Arquitectura: Transacciones Distribuidas (Patrón Saga)
- Mauricio ECR
- Arquitectura
- 14 Feb, 2026
PARTE I: PRINCIPIOS Y NORMATIVA 1. Fundamentos de Consistencia Eventual Debido a la naturaleza distribuida del sistema, se abandona el modelo ACID tradicional (Atomicidad inmediata con bloque
Estándar de Arquitectura: Transacciones Distribuidas (Patrón Saga)
- Mauricio ECR
- Arquitectura
- 14 Feb, 2026
PARTE I: PRINCIPIOS Y NORMATIVA
1. Fundamentos de Consistencia Eventual
Debido a la naturaleza distribuida del sistema, se abandona el modelo ACID tradicional (Atomicidad inmediata con bloqueos) en favor del modelo BASE (Basically Available, Soft state, Eventually consistent).
Implicaciones arquitectónicas:
- Los datos pueden estar temporalmente inconsistentes entre servicios
- La consistencia se alcanza mediante propagación de eventos y compensaciones
- Cada servicio mantiene su propia fuente de verdad (base de datos)
- No existen transacciones atómicas que abarquen múltiples servicios
Garantías del sistema:
- Disponibilidad: Los servicios responden incluso si otros están caídos
- Durabilidad: Los eventos se persisten antes de considerarse publicados
- Convergencia: El sistema eventualmente alcanzará un estado consistente
2. Patrones Permitidos
2.1 Patrón Primario: Saga basada en Coreografía
Los servicios participantes reaccionan a eventos de dominio de manera autónoma sin conocer el flujo completo de la transacción distribuida. Cada servicio:
- Escucha eventos relevantes de su dominio
- Ejecuta su lógica de negocio local
- Publica eventos de resultado
- No tiene conocimiento de qué otros servicios participan en el proceso
Ventajas: Bajo acoplamiento, alta escalabilidad, sin punto único de fallo.
Desventajas: Flujo implícito, difícil depuración, complejidad en el rastreo.
2.2 Patrón Complementario: Orquestación Ligera de Estado
Se permite que UN servicio actúe como Coordinador de Estado (no como orquestador tradicional) con las siguientes restricciones:
Permitido:
- Mantener una tabla de estado que rastree el progreso de la saga
- Escuchar todos los eventos relevantes del flujo
- Tomar decisiones de cancelación basadas en eventos recibidos
- Emitir comandos de compensación cuando sea necesario
- Proveer endpoints de consulta del estado de la transacción
Prohibido:
- Invocar directamente (HTTP/gRPC) a otros servicios para ejecutar pasos
- Mantener lógica de negocio que corresponde a otros dominios
- Actuar como proxy o gateway entre servicios
Clarificación: El coordinador observa y reacciona, no comanda y espera. La comunicación sigue siendo asíncrona mediante el bus de eventos.
2.3 Prohibiciones Absolutas
- Two-Phase Commit (2PC): No se permite debido a bloqueos prolongados y baja disponibilidad
- XA Transactions: Prohibido extender transacciones de base de datos entre servicios
- Distributed Locks: No se permiten bloqueos compartidos entre servicios (excepto Semantic Locks de negocio, ver Parte III)
- Llamadas Síncronas en Flujo Crítico: HTTP/REST/gRPC solo para consultas (queries), nunca para comandos (writes) en sagas
3. Comunicación y Mensajería
3.1 Intermediario Obligatorio
Toda comunicación entre pasos de una saga debe realizarse a través de un Message Broker con las siguientes características:
Requisitos mínimos del broker:
- Persistencia en disco (durabilidad de mensajes)
- Garantía de entrega "al menos una vez" (at-least-once delivery)
- Capacidad de reintento automático
- Soporte para Dead Letter Queues (DLQ)
- Ordenamiento por partición (opcional pero recomendado)
Brokers aprobados: Kafka, RabbitMQ, Amazon SQS/SNS, Azure Service Bus, Google Pub/Sub.
3.2 Topología de Mensajería
Para eventos de dominio:
- Utilizar patrón Publish-Subscribe (pub/sub)
- Múltiples consumidores pueden suscribirse al mismo evento
- Los productores no conocen a los consumidores
Para comandos directos (casos excepcionales):
- Utilizar colas punto-a-punto
- Un solo consumidor procesa el mensaje
- Incluir timeout de procesamiento
3.3 Restricciones de Comunicación Síncrona
Prohibido para:
- Ejecutar el siguiente paso de una transacción crítica
- Confirmar operaciones de escritura entre servicios
- Propagar cambios de estado en flujos transaccionales
Permitido para:
- Consultas de solo lectura (queries)
- Validaciones previas no bloqueantes
- Obtención de datos de referencia
- Healthchecks y monitoreo
4. Transactional Outbox Pattern (Obligatorio)
4.1 Definición del Problema
El Dual Write Problem ocurre cuando un servicio intenta:
- Actualizar su base de datos local
- Publicar un evento en el broker
Si la publicación falla después del commit de BD, el sistema queda inconsistente. Si falla antes, se pierde el evento.
4.2 Solución Mandatoria
Regla de Oro: Un servicio nunca debe publicar directamente en el broker dentro de su código de negocio.
Implementación del patrón:
Primera fase - Transacción Atómica Local:
- Iniciar transacción de base de datos
- Ejecutar operación de negocio (INSERT/UPDATE/DELETE en tablas de dominio)
- Insertar el evento a publicar en una tabla especial llamada OUTBOX
- Confirmar transacción completa (COMMIT atómico)
Segunda fase - Publicación Asíncrona: 5. Un proceso independiente (Relay/Publisher) lee continuamente la tabla OUTBOX 6. Publica los eventos pendientes en el broker 7. Marca los eventos como publicados o los elimina
Tabla OUTBOX - Estructura requerida:
Campos obligatorios:
- Identificador único del mensaje (UUID)
- Tipo de evento (nombre del evento de dominio)
- Cuerpo del evento (payload serializado)
- Timestamp de creación
- Estado de publicación (pendiente, publicado, fallido)
- Número de intentos de publicación
- Agregado raíz asociado (para ordenamiento)
- Versión del esquema del evento
4.3 Estrategias de Relay
Opción A - Polling:
- Proceso que consulta periódicamente la tabla OUTBOX
- Publica eventos pendientes ordenados por timestamp
- Marca como publicados tras confirmación del broker
- Intervalo recomendado: 100-500 milisegundos
Opción B - Change Data Capture (CDC):
- Herramienta que lee el transaction log de la base de datos
- Detecta inserts en OUTBOX en tiempo real
- Publica automáticamente en el broker
- Ejemplos: Debezium, Maxwell, AWS DMS
Opción C - Database Triggers:
- Trigger que se activa al insertar en OUTBOX
- Invoca procedimiento que publica en broker
- No recomendado por acoplamiento y menor resiliencia
5. Resiliencia e Idempotencia
5.1 Principio de Idempotencia
Dado que los brokers garantizan entrega "al menos una vez", es inevitable que algunos mensajes se entreguen duplicados. Todo consumidor de eventos DEBE ser idempotente.
Definición: Una operación es idempotente si ejecutarla múltiples veces produce el mismo resultado que ejecutarla una sola vez.
Verificación obligatoria: Antes de procesar un evento, el consumidor debe verificar si el identificador del mensaje ya fue procesado previamente.
5.2 Implementación de Deduplicación
Tabla de Registro de Mensajes Procesados:
Cada servicio debe mantener una tabla dedicada con:
- Identificador del mensaje (clave primaria)
- Tipo de evento procesado
- Timestamp de procesamiento
- Estado final (éxito/fallo)
- Índice en timestamp para limpieza periódica
Flujo de procesamiento idempotente:
- Recibir mensaje del broker
- Iniciar transacción de base de datos local
- Intentar insertar el identificador del mensaje en la tabla de registro
- Si la inserción falla por duplicado: hacer rollback y retornar éxito (ya fue procesado)
- Si la inserción es exitosa: ejecutar lógica de negocio
- Insertar evento resultante en tabla OUTBOX (si aplica)
- Confirmar transacción completa
- Enviar ACK al broker
Política de limpieza: Eliminar registros con más de siete días de antigüedad mediante proceso nocturno.
5.3 Estrategias de Compensación
Para toda operación de escritura que modifique estado de negocio, el servicio debe implementar una Transacción Compensatoria.
Definición: Acción lógicamente inversa que deshace (o mitiga) el efecto de una operación previamente confirmada.
Ejemplos de compensación:
Operación original → Compensación:
- CrearPedido → AnularPedido
- ReservarInventario → LiberarReserva
- CobrarPago → ReembolsarPago
- EnviarNotificacion → EnviarNotificacionCorreccion
- AsignarRecurso → DesasignarRecurso
Características de compensaciones:
- Debe ser idempotente (puede ejecutarse múltiples veces)
- Puede ser semántica (no necesariamente restaura estado exacto)
- Debe registrarse en logs de auditoría
- Debe emitir eventos de compensación para trazabilidad
Tipos de compensación:
- Compensación perfecta: Restaura el estado exacto anterior (ej: cancelar reserva)
- Compensación aproximada: Restaura un estado equivalente (ej: reembolso en créditos en vez de dinero)
- Compensación simbólica: Registra el intento de reversión cuando la compensación real es imposible (ej: no se puede "des-enviar" un email, pero se envía corrección)
5.4 Manejo de Errores y Reintentos
Clasificación de fallos:
Fallos Transitorios: Errores temporales que pueden resolverse reintentando
- Pérdida de conexión de red
- Timeouts de base de datos por carga
- Servicio dependiente temporalmente no disponible
- Límites de rate limiting
Fallos Permanentes: Errores que no se resolverán reintentando
- Validaciones de negocio fallidas
- Datos malformados o incompletos
- Violaciones de reglas de dominio
- Permisos insuficientes
- Recursos no encontrados
Estrategia de Reintentos - Exponential Backoff:
Para fallos transitorios se debe implementar:
- Espera inicial entre primer y segundo intento: 500 milisegundos
- Multiplicador exponencial: factor de 2
- Espera máxima entre intentos (techo): 60 segundos
- Número máximo de intentos: 5
- Jitter aleatorio: añadir variación del 10-25% para evitar thundering herd
Progresión ejemplo: 500ms → 1s → 2s → 4s → 8s → DLQ
Dead Letter Queue (DLQ):
Después de agotar los reintentos, el mensaje debe enviarse a una cola especial para:
- Análisis manual posterior
- Alertas al equipo de operaciones
- Posible reprocesamiento manual tras corrección
- Auditoría de fallos recurrentes
Propiedades requeridas en mensajes de DLQ:
- Mensaje original completo
- Número de intentos realizados
- Timestamps de cada intento
- Detalles de cada error ocurrido
- Trace completo del último error
6. Versionado y Evolución de Contratos
6.1 Esquemas Explícitos Obligatorios
Todo evento de dominio publicado en el bus debe tener un esquema formal que defina:
- Nombre y tipo de cada campo
- Campos obligatorios vs opcionales
- Tipos de datos permitidos
- Restricciones de validación
- Descripción semántica de cada campo
Formatos aprobados: Avro, Protocol Buffers (Protobuf), JSON Schema.
Prohibido: Publicar eventos con estructura ad-hoc sin definición formal.
6.2 Versionado Semántico de Eventos
Todo evento debe incluir un campo de metadatos que indique su versión siguiendo el formato semántico: MAJOR.MINOR.PATCH
Ejemplo: "schema_version": "1.2.0"
Interpretación de versiones:
MAJOR: Cambios incompatibles que requieren actualización del consumidor
- Eliminar campos
- Cambiar tipo de dato existente
- Cambiar semántica del campo
- Renombrar campos
MINOR: Cambios retrocompatibles que agregan funcionalidad
- Agregar nuevos campos opcionales
- Agregar nuevos valores a enumeraciones
- Deprecar campos (sin eliminarlos)
PATCH: Correcciones menores sin impacto funcional
- Corregir descripciones
- Mejorar documentación
- Correcciones de typos en nombres
6.3 Estrategias de Evolución
Para cambios ADITIVOS (Minor/Patch):
- Agregar solo campos opcionales con valores por defecto
- Los consumidores antiguos ignoran campos nuevos
- Los productores nuevos deben tolerar consumidores antiguos
- No requiere coordinación de despliegue
Para cambios BREAKING (Major):
- Crear un nuevo tipo de evento con sufijo de versión
- Ejemplo: "OrdenSolicitada_v2"
- Mantener publicación dual por período de transición
- El productor emite tanto evento v1 como v2
- Los consumidores migran gradualmente a la nueva versión
- Período mínimo de convivencia: 90 días calendario
- Después del período, deprecar y eliminar versión antigua
Política de deprecación:
- Anunciar deprecación con 90 días de anticipación
- Añadir warnings en logs cuando se use versión antigua
- Publicar métricas de uso de versiones obsoletas
- Coordinar migración con todos los equipos consumidores
- Eliminar soporte solo cuando uso sea cero por 30 días
6.4 Registro Centralizado de Esquemas
Obligatorio: Mantener un Schema Registry centralizado que:
- Almacena todas las versiones de esquemas de eventos
- Valida compatibilidad antes de registrar nuevas versiones
- Provee APIs para consulta programática de esquemas
- Genera documentación automática de contratos
- Permite validación en tiempo de runtime
Herramientas recomendadas: Confluent Schema Registry, AWS Glue Schema Registry, Apicurio Registry.
7. Observabilidad y Rastreabilidad
7.1 Rastreo Distribuido
Generación de Identificadores:
El servicio que inicia una saga debe generar los siguientes identificadores únicos:
Saga ID: Identificador global único (UUID versión 4) que representa toda la transacción distribuida. Se genera una sola vez al inicio y se propaga sin cambios.
Span ID: Identificador único para cada paso o evento individual dentro de la saga. Cada servicio que procesa genera su propio Span ID.
Parent Span ID: Referencia al Span ID del paso anterior, creando una jerarquía de trazas.
Propagación de Contexto:
Estos identificadores deben incluirse como headers/metadatos en todos los mensajes:
- Nombre del header de Saga ID: "X-Saga-ID"
- Nombre del header de Span ID: "X-Span-ID"
- Nombre del header de Parent Span: "X-Parent-Span-ID"
Los servicios intermedios deben:
- Preservar el Saga ID sin modificarlo
- Generar su propio Span ID
- Copiar el Span ID recibido como su Parent Span ID
- Propagar estos tres valores en todos los eventos que emitan
7.2 Logging Estructurado
Formato Obligatorio:
Cada entrada de log relacionada con procesamiento de eventos de saga debe ser estructurada (no texto plano) e incluir los siguientes campos:
Campos mandatorios de contexto:
- Identificador de saga (copiado del mensaje)
- Tipo de evento procesado
- Versión del esquema del evento
- Nombre del servicio que genera el log
- Timestamp en formato ISO-8601 con zona horaria UTC
- Identificador del span actual
- Identificador del span padre
Campos mandatorios de resultado:
- Estado del procesamiento: RECEIVED, PROCESSING, SUCCESS, FAILED, COMPENSATING, COMPENSATED
- Duración en milisegundos de la operación
- Número de intento (para reintentos)
Campos opcionales pero recomendados:
- Identificador de correlación de negocio (ej: número de orden)
- Identificador del usuario o entidad que inició la transacción
- Datos relevantes del payload (sin información sensible)
- Detalles del error (en caso de fallo)
- Nombre del nodo/instancia que procesó
Niveles de Log:
- INFO: Inicio y fin exitoso de procesamiento de evento
- WARN: Reintentos por fallos transitorios
- ERROR: Fallos permanentes, envío a DLQ
- DEBUG: Detalles de validaciones y decisiones de negocio
Ejemplo descriptivo de entrada de log:
Un log estructurado indicando que el servicio de Inventario procesó exitosamente un evento PagoExitoso en 150 milisegundos, este fue el primer intento, pertenece a la saga con ID alfa-123, el span actual es beta-456 hijo del span gamma-789, ocurrió el 7 de febrero de 2026 a las 10:30:00 UTC, procesó la versión 1.0 del evento, y resultó en éxito.
7.3 Métricas Requeridas
Todos los servicios participantes en sagas deben exponer las siguientes métricas en formato compatible con sistemas de monitoreo:
Métricas de duración:
- Nombre: saga_duration_seconds
- Tipo: Histogram
- Etiquetas: tipo_de_saga, estado_final (success, failed, compensated)
- Descripción: Tiempo total desde inicio hasta conclusión de la saga
Métricas de errores por paso:
- Nombre: saga_step_errors_total
- Tipo: Counter (contador acumulativo)
- Etiquetas: nombre_servicio, tipo_evento, tipo_error
- Descripción: Cantidad total de fallos al procesar eventos
Métricas de compensaciones:
- Nombre: saga_compensations_total
- Tipo: Counter
- Etiquetas: nombre_servicio, razón_compensación
- Descripción: Cantidad de transacciones compensatorias ejecutadas
Métricas de mensajes pendientes:
- Nombre: outbox_pending_messages
- Tipo: Gauge (valor instantáneo)
- Etiquetas: nombre_servicio
- Descripción: Cantidad de eventos en tabla OUTBOX pendientes de publicar
Métricas de mensajes en DLQ:
- Nombre: dlq_messages_total
- Tipo: Gauge
- Etiquetas: nombre_servicio, tipo_evento
- Descripción: Cantidad de mensajes en Dead Letter Queue por servicio
Formato de exportación: Prometheus, OpenMetrics, o CloudWatch.
7.4 Trazabilidad de Auditoría
Para procesos críticos de negocio se debe mantener:
- Tabla de auditoría de saga con todos los cambios de estado
- Timestamp de cada transición de estado
- Razón del cambio (evento que lo provocó)
- Usuario o sistema responsable del inicio
- Datos relevantes de negocio (sin información sensible duplicada)
- Retención mínima: según políticas regulatorias (típicamente 7 años)
8. Límites Operacionales y Timeouts
8.1 Parámetros Configurables Mandatorios
Todo servicio participante en sagas debe exponer y documentar los siguientes parámetros de configuración:
Reintentos:
Número máximo de intentos antes de enviar a DLQ
- Valor mínimo permitido: 3 intentos
- Valor recomendado: 5 intentos
Espera inicial entre primer y segundo intento (backoff inicial)
- Valor mínimo permitido: 100 milisegundos
- Valor recomendado: 500 milisegundos
Espera máxima entre intentos (techo de backoff)
- Valor mínimo permitido: 30 segundos
- Valor recomendado: 60 segundos
Timeouts de saga completa:
- Tiempo máximo total para completar toda la saga
- Valor mínimo permitido: 1 hora
- Valor recomendado: 24 horas
- Nota: Ajustar según naturaleza del proceso de negocio
Timeouts por paso individual:
- Tiempo máximo de espera por respuesta de un paso
- Valor mínimo permitido: 30 segundos
- Valor recomendado: 5 minutos (300 segundos)
Estos valores deben ser configurables sin recompilar código (variables de entorno, archivos de configuración, configuration server).
8.2 Política de Timeouts
Para timeouts de paso individual:
Si un evento esperado no llega dentro del plazo configurado:
- El servicio coordinador (si existe) debe emitir un evento de timeout
- Nombre del evento: "StepTimedOut" o similar
- Incluir en el payload: saga_id, paso esperado, tiempo transcurrido
- Iniciar proceso de compensación
- Registrar en logs con nivel ERROR
- Incrementar métrica de timeouts
Para timeouts de saga completa:
Si la saga no se completa dentro del plazo total configurado:
- Ejecutar compensación automática de todos los pasos confirmados
- Marcar la saga con estado TIMED_OUT
- Emitir evento de saga expirada para auditoría
- Notificar al usuario/sistema iniciador del fallo
- Generar alerta para equipo de operaciones
- No eliminar datos de auditoría (mantener para análisis)
8.3 Monitoreo de Umbrales
Alertas obligatorias:
- Si el porcentaje de sagas fallidas supera 5% en ventana de 15 minutos
- Si el tiempo promedio de saga supera el doble del baseline histórico
- Si la cantidad de mensajes en DLQ supera 10 por servicio
- Si hay mensajes en OUTBOX pendientes por más de 10 minutos
- Si una saga individual supera el 80% del timeout configurado
Niveles de severidad:
- CRITICAL: Afecta flujos de negocio críticos (pagos, pedidos)
- HIGH: Afecta funcionalidad importante pero no crítica
- MEDIUM: Degradación de rendimiento sin pérdida de funcionalidad
- LOW: Anomalías detectadas pero sin impacto inmediato
PARTE II: IMPLEMENTACIÓN DE REFERENCIA
1. Caso de Uso: Procesamiento de Órdenes con Validación Preventiva
Dominio de negocio: Sistema de comercio electrónico
Objetivo: Procesar una orden de compra asegurando disponibilidad de inventario antes de ejecutar el cobro, minimizando reembolsos por falta de stock.
Estrategia elegida: Check-Then-Act (Verificación antes de Acción Financiera)
Justificación: Reducir costos de transacciones bancarias fallidas y mejorar experiencia del cliente evitando cobros seguidos de reembolsos.
2. Definición del Flujo Transaccional
El proceso se divide en cuatro fases secuenciales:
Fase 1 - Intención:
- Acción: Registro inicial de la orden en el sistema
- Estado resultante: PENDIENTE_VALIDACION
- Evento emitido: OrdenSolicitada
Fase 2 - Validación:
- Acción: Consulta de disponibilidad de inventario sin reserva
- Tipo de operación: Lectura (SELECT) sin bloqueos
- Eventos posibles: StockVerificado o StockNoDisponible
Fase 3 - Cobro Condicional:
- Acción: Ejecución de transacción financiera
- Precondición: Solo si Fase 2 fue exitosa
- Eventos posibles: PagoExitoso o PagoRechazado
Fase 4 - Asignación con Bloqueo:
- Acción: Descuento definitivo de inventario
- Tipo de operación: Escritura (UPDATE) con bloqueo pesimista
- Eventos posibles: StockAsignado o FalloAsignacion
3. Servicios Participantes y Responsabilidades
3.1 Servicio: Gestor de Pedidos
Rol: Coordinador de estado (Orquestación Ligera)
Responsabilidades:
- Recibir la solicitud inicial del cliente
- Crear el registro de orden con estado inicial
- Generar el Saga ID único
- Emitir el evento OrdenSolicitada vía patrón Outbox
- Escuchar eventos de progreso del resto de participantes
- Mantener máquina de estados de la orden
- Actualizar estado según eventos recibidos
- Notificar al cliente sobre el resultado final
Transiciones de estado:
Estado PENDIENTE_VALIDACION al recibir:
- StockVerificado → PENDIENTE_PAGO
- StockNoDisponible → CANCELADA_SIN_STOCK (flujo termina)
- Timeout de validación → CANCELADA_TIMEOUT
Estado PENDIENTE_PAGO al recibir:
- PagoExitoso → PENDIENTE_ASIGNACION
- PagoRechazado → CANCELADA_PAGO_RECHAZADO
Estado PENDIENTE_ASIGNACION al recibir:
- StockAsignado → COMPLETADA (flujo exitoso)
- FalloAsignacion → CANCELADA_CON_REEMBOLSO (requiere compensación)
Eventos que escucha:
- StockVerificado
- StockNoDisponible
- PagoExitoso
- PagoRechazado
- StockAsignado
- FalloAsignacion
- ReembolsoEjecutado
Eventos que emite:
- OrdenSolicitada (inicio del flujo)
- OrdenCompletada (conclusión exitosa)
- OrdenCancelada (conclusión con fallo)
3.2 Servicio: Gestor de Inventario
Rol: Validador y Ejecutor (participa en dos momentos diferentes)
Responsabilidades:
Momento 1 - Validación (Fase 2):
- Escuchar evento OrdenSolicitada
- Consultar disponibilidad actual en base de datos
- Validar si existe stock suficiente para los ítems solicitados
- NO realizar ninguna reserva ni modificación de datos
- Emitir resultado de validación
Momento 2 - Asignación (Fase 4):
- Escuchar evento PagoExitoso
- Iniciar transacción de base de datos con bloqueo pesimista
- Re-verificar disponibilidad actual (puede haber cambiado desde Fase 2)
- Descontar las unidades si aún hay stock disponible
- Confirmar transacción o hacer rollback según resultado
- Emitir resultado de asignación
Lógica de validación (Momento 1):
Para cada ítem en la orden:
- Consultar tabla de productos con el SKU solicitado
- Leer campo de cantidad disponible actual
- Comparar cantidad solicitada vs cantidad disponible
- Si para TODOS los ítems hay stock suficiente: emitir StockVerificado
- Si para ALGÚN ítem no hay stock suficiente: emitir StockNoDisponible con detalles
Lógica de asignación (Momento 2):
- Iniciar transacción con nivel de aislamiento REPEATABLE_READ o superior
- Aplicar bloqueo pesimista (SELECT FOR UPDATE) sobre los productos afectados
- Re-leer cantidad disponible actual
- Validar nuevamente que hay stock suficiente
- Si validación exitosa:
- Ejecutar UPDATE restando las unidades
- Insertar evento StockAsignado en tabla OUTBOX
- COMMIT de transacción
- Si validación falla (race condition, stock consumido por otra transacción):
- Insertar evento FalloAsignacion en tabla OUTBOX
- COMMIT de transacción (el evento de fallo debe publicarse)
Eventos que escucha:
- OrdenSolicitada (trigger de validación)
- PagoExitoso (trigger de asignación)
Eventos que emite:
- StockVerificado
- StockNoDisponible
- StockAsignado
- FalloAsignacion
Compensación: Si recibe evento de compensación (por fallo en paso posterior):
- Restaurar las unidades de inventario sumando la cantidad original
- Emitir evento StockLiberado
3.3 Servicio: Procesador de Pagos
Rol: Intermediario financiero condicional
Responsabilidades:
Operación Normal:
- Escuchar evento StockVerificado (NO OrdenSolicitada, para evitar cobros sin stock)
- Extraer información de pago del payload del evento
- Invocar API de pasarela de pagos externa (Stripe, PayPal, etc.)
- Manejar respuesta de la pasarela
- Emitir resultado de la operación financiera
Operación de Compensación:
- Escuchar evento FalloAsignacion
- Identificar la transacción financiera original asociada
- Ejecutar reembolso o reversa en la pasarela de pagos
- Emitir evento de confirmación de compensación
Lógica de cobro:
- Recibir evento StockVerificado
- Validar que evento no fue procesado previamente (idempotencia)
- Extraer datos: monto, método de pago, token de tarjeta, etc.
- Llamar API de pasarela con timeout de 30 segundos
- Si respuesta es exitosa:
- Almacenar ID de transacción de la pasarela
- Insertar evento PagoExitoso en OUTBOX con referencia de transacción
- COMMIT
- Si respuesta es rechazo (fondos insuficientes, tarjeta inválida):
- Insertar evento PagoRechazado en OUTBOX con código de error
- COMMIT
- Si hay timeout o error de red:
- Aplicar política de reintentos con exponential backoff
- Tras agotar intentos: enviar a DLQ para revisión manual
Lógica de compensación:
- Recibir evento FalloAsignacion
- Extraer Saga ID para identificar la transacción financiera original
- Buscar en base de datos local el ID de transacción de la pasarela
- Llamar API de reembolso de la pasarela con el ID original
- Si reembolso exitoso:
- Insertar evento ReembolsoEjecutado en OUTBOX
- COMMIT
- Si reembolso falla:
- Reintentar con backoff exponencial
- Tras 5 intentos fallidos: enviar alerta crítica a equipo de finanzas
- Marcar para reembolso manual
Eventos que escucha:
- StockVerificado (trigger de cobro)
- FalloAsignacion (trigger de compensación)
Eventos que emite:
- PagoExitoso
- PagoRechazado
- ReembolsoEjecutado
- FalloReembolso (para casos críticos)
4. Escenarios de Ejecución
4.1 Escenario A: Flujo Exitoso (Happy Path)
Contexto inicial:
- Cliente solicita 1 unidad del producto SKU-123
- Inventario actual: 10 unidades disponibles
- No hay operaciones concurrentes
Secuencia de eventos:
Cliente envía solicitud HTTP POST al endpoint de Pedidos
Pedidos crea registro de orden con estado PENDIENTE_VALIDACION
Pedidos genera Saga ID: "550e8400-e29b-41d4-a716-446655440000"
Pedidos inserta en OUTBOX el evento OrdenSolicitada
Relay de Pedidos publica evento en topic "order-events"
Inventario consume evento OrdenSolicitada
Inventario consulta: SELECT cantidad FROM productos WHERE sku = 'SKU-123'
Inventario verifica: 10 unidades disponibles, pedido requiere 1, verificación OK
Inventario inserta en OUTBOX el evento StockVerificado
Relay de Inventario publica evento en topic "inventory-events"
Pedidos consume StockVerificado y actualiza estado a PENDIENTE_PAGO
Pagos consume StockVerificado
Pagos invoca API de pasarela: "Cobrar 50.00 USD a tarjeta terminada en 4242"
Pasarela responde: "Aprobado, transaction_id: txn_abc123"
Pagos inserta en OUTBOX el evento PagoExitoso con referencia txn_abc123
Relay de Pagos publica evento en topic "payment-events"
Pedidos consume PagoExitoso y actualiza estado a PENDIENTE_ASIGNACION
Inventario consume PagoExitoso
Inventario inicia transacción con: BEGIN; SELECT cantidad FROM productos WHERE sku = 'SKU-123' FOR UPDATE
Inventario lee cantidad bloqueada: 10 unidades
Inventario valida: 10 >= 1, OK
Inventario ejecuta: UPDATE productos SET cantidad = cantidad - 1 WHERE sku = 'SKU-123'
Inventario inserta en OUTBOX el evento StockAsignado
Inventario confirma: COMMIT
Relay de Inventario publica evento en topic "inventory-events"
Pedidos consume StockAsignado y actualiza estado a COMPLETADA
Pedidos emite evento OrdenCompletada
Pedidos envía notificación al cliente: "Tu orden ha sido confirmada"
Resultado final:
- Orden completada exitosamente
- Inventario reducido a 9 unidades
- Cliente cobrado y notificado
- Duración total: aproximadamente 2-5 segundos
4.2 Escenario B: Fallo Temprano (Sin Stock Disponible)
Contexto inicial:
- Cliente solicita 5 unidades del producto SKU-456
- Inventario actual: 0 unidades disponibles
- Optimización: evitar cobro innecesario
Secuencia de eventos:
Cliente envía solicitud HTTP POST al endpoint de Pedidos
Pedidos crea registro de orden con estado PENDIENTE_VALIDACION
Pedidos genera Saga ID: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
Pedidos inserta en OUTBOX el evento OrdenSolicitada
Relay de Pedidos publica evento en topic "order-events"
Inventario consume evento OrdenSolicitada
Inventario consulta: SELECT cantidad FROM productos WHERE sku = 'SKU-456'
Inventario verifica: 0 unidades disponibles, pedido requiere 5, verificación FALLA
Inventario inserta en OUTBOX el evento StockNoDisponible con detalles
Relay de Inventario publica evento en topic "inventory-events"
Pedidos consume StockNoDisponible
Pedidos actualiza estado a CANCELADA_SIN_STOCK
Pedidos emite evento OrdenCancelada con razón "stock insuficiente"
Pedidos envía notificación al cliente: "Lo sentimos, el producto está agotado"
Comportamiento de Pagos:
- El servicio de Pagos NO escucha el evento StockNoDisponible
- Por tanto, NO se ejecuta ninguna operación financiera
- No hay cargos ni reembolsos
Resultado final:
- Orden cancelada rápidamente (en menos de 1 segundo)
- Cliente no fue cobrado
- Costo financiero: cero
- Experiencia de cliente: transparente y honesta
Beneficio del patrón: Esta arquitectura evita aproximadamente el 95% de reembolsos comparado con estrategias de "cobrar primero, verificar después".
4.3 Escenario C: Fallo Tardío (Race Condition)
Contexto inicial:
- Cliente A solicita 1 unidad del producto SKU-789
- Inventario actual: 1 unidad disponible
- Evento concurrente: Cliente B también solicita 1 unidad del mismo producto
Secuencia de eventos para Cliente A:
Pedidos A crea orden con Saga ID: "saga-aaa"
Pedidos A emite OrdenSolicitada
Inventario verifica disponibilidad para saga-aaa
Inventario consulta: 1 unidad disponible
Inventario emite StockVerificado para saga-aaa
Pagos procesa cobro para saga-aaa
Pasarela aprueba transacción: txn_xyz789
Pagos emite PagoExitoso para saga-aaa
Evento concurrente (Cliente B):
Mientras el flujo de Cliente A está entre Fase 3 y Fase 4:
- Pedidos B emite OrdenSolicitada para saga-bbb
- Inventario verifica: aún hay 1 unidad, emite StockVerificado para saga-bbb
- Pagos cobra a Cliente B: txn_def456
- Pagos emite PagoExitoso para saga-bbb
Continuación para Cliente A (intenta asignar primero):
Inventario consume PagoExitoso para saga-aaa
Inventario inicia: BEGIN; SELECT cantidad FROM productos WHERE sku = 'SKU-789' FOR UPDATE
Inventario lee cantidad: 1 unidad
Inventario valida: 1 >= 1, OK
Inventario ejecuta: UPDATE productos SET cantidad = 0
Inventario inserta evento StockAsignado para saga-aaa
Inventario confirma: COMMIT
Pedidos A recibe StockAsignado
Orden A finaliza con estado COMPLETADA
Continuación para Cliente B (llega segundo):
Inventario consume PagoExitoso para saga-bbb
Inventario inicia: BEGIN; SELECT cantidad FROM productos WHERE sku = 'SKU-789' FOR UPDATE
Inventario lee cantidad: 0 unidades (ya fue consumida por Cliente A)
Inventario valida: 0 >= 1, FALLA
Inventario NO ejecuta UPDATE
Inventario inserta evento FalloAsignacion para saga-bbb con razón "race condition"
Inventario confirma: COMMIT (importante: confirmar para que evento se publique)
Pagos consume FalloAsignacion para saga-bbb
Pagos busca transacción original: txn_def456
Pagos invoca API de pasarela: "Reembolsar txn_def456"
Pasarela confirma: "Reembolso procesado, refund_id: rfnd_xyz"
Pagos inserta evento ReembolsoEjecutado para saga-bbb
Relay de Pagos publica evento
Pedidos B consume ReembolsoEjecutado
Pedidos B actualiza estado a CANCELADA_CON_REEMBOLSO
Pedidos B envía notificación al Cliente B: "Tu pago ha sido reembolsado, el producto se agotó durante el proceso"
Resultado final:
- Cliente A: orden completada, inventario asignado
- Cliente B: orden cancelada, dinero reembolsado automáticamente
- Sistema mantuvo consistencia a pesar de concurrencia
- No hubo sobreventa (overselling)
Observación crítica: Este escenario demuestra por qué la verificación en Fase 2 es "optimista" (sin bloqueo) y la asignación en Fase 4 es "pesimista" (con bloqueo). El bloqueo temprano causaría alta contención. El bloqueo tardío minimiza ventana crítica.
5. Consideraciones de Implementación
5.1 Ordenamiento de Eventos
Problema: Los brokers garantizan orden dentro de una partición, no globalmente.
Solución para este caso de uso:
- Particionar eventos por Saga ID
- Todos los eventos de una misma saga van a la misma partición
- Configurar clave de partición: Saga ID
- Garantiza que eventos de UNA orden se procesan en orden
- No importa el orden entre órdenes diferentes
5.2 Manejo de Duplicados
Ejemplo de deduplicación en Inventario:
Cuando llega evento PagoExitoso:
- Extraer Message ID del header: "msg-12345"
- Iniciar transacción
- Intentar: INSERT INTO processed_messages (message_id, event_type) VALUES ('msg-12345', 'PagoExitoso')
- Si falla por clave duplicada:
- Significa que este mensaje ya fue procesado
- Hacer ROLLBACK
- Retornar ACK al broker (no reintentar)
- Registrar en log: "Evento duplicado ignorado"
- Si inserción exitosa:
- Proceder con lógica de asignación de stock
- COMMIT incluye tanto la nueva fila en processed_messages como el UPDATE de inventario
- Retornar ACK al broker
5.3 Consistencia de OUTBOX
Garantía crítica: El evento en OUTBOX y el cambio de estado de negocio deben confirmarse en la misma transacción atómica de base de datos.
Ejemplo en Pedidos al recibir StockVerificado:
Transacción única:
- BEGIN
- UPDATE orders SET status = 'PENDIENTE_PAGO' WHERE saga_id = '550e8400...'
- INSERT INTO outbox (event_type, payload, saga_id) VALUES ('OrdenActualizada', '{...}', '550e8400...')
- COMMIT
Si falla el COMMIT, ningún cambio se persiste. Si se confirma, ambos cambios quedan guardados atómicamente.
5.4 Configuración de Timeouts por Servicio
Pedidos:
- Timeout de validación de stock: 30 segundos
- Timeout de pago: 60 segundos (APIs bancarias pueden ser lentas)
- Timeout de asignación: 15 segundos
- Timeout total de saga: 2 horas
Inventario:
- Timeout de consulta de BD: 5 segundos
- Timeout de bloqueo pesimista: 10 segundos
Pagos:
- Timeout de llamada a pasarela: 30 segundos
- Reintentos en pasarela: 3 intentos con backoff de 2-8-18 segundos
- Timeout de reembolso: 60 segundos
PARTE III: PATRONES AVANZADOS (OPCIONAL)
1. Sagas de Larga Duración
Definición: Procesos que requieren más de una hora para completarse, típicamente por intervención humana, validaciones externas, o pasos asíncronos lentos.
Ejemplos:
- Proceso de aprobación de crédito (requiere revisión manual)
- Workflow de incorporación de empleado (múltiples pasos en días)
- Proceso de compra B2B con aprobaciones corporativas
- Integración con sistemas legacy batch que procesan nocturnamente
1.1 Desafíos Específicos
Problema 1: Estado en Memoria No es viable mantener el estado de la saga en memoria de un servicio durante horas o días. El servicio puede reiniciarse.
Problema 2: Escalabilidad Miles de sagas activas simultáneas durante días consumen recursos si no se gestionan apropiadamente.
Problema 3: Visibilidad Usuarios y operadores necesitan consultar el estado de procesos que toman días.
1.2 Estrategia de Implementación
Persistencia de Estado Explícita:
Crear una tabla dedicada para rastrear sagas activas:
Campos requeridos:
- Identificador único de saga (PK)
- Tipo de saga (ej: "AprobacionCredito")
- Estado actual (ej: "ESPERANDO_REVISION_MANUAL")
- Timestamp de inicio
- Timestamp de última actualización
- Paso actual en el flujo
- Contexto de negocio (datos necesarios para reanudar)
- Usuario o entidad propietaria
- Fecha de expiración o timeout
Timers Persistentes:
En lugar de mantener timers en memoria, usar:
- Scheduled jobs que consultan tabla de sagas periódicamente
- Buscar sagas en estado de espera cuyo timeout ha expirado
- Emitir eventos de timeout para reanudar o compensar
- Ejemplo: Job cada 5 minutos revisa sagas con "expected_event_by" < NOW()
Endpoints de Consulta:
Exponer APIs REST para que usuarios consulten estado:
- GET /sagas/saga-id/status → Retorna estado actual y progreso
- GET /sagas/saga-id/history → Retorna todos los eventos y transiciones
- POST /sagas/saga-id/cancel → Permite cancelación manual (ejecuta compensación)
1.3 Patrón de Reanudación
Caso de uso: Proceso pausado esperando aprobación humana.
Flujo:
- Saga llega a paso que requiere aprobación
- Servicio emite evento "AprobacionSolicitada"
- Servicio actualiza tabla de sagas: estado = "ESPERANDO_APROBACION"
- Se envía notificación a aprobador (email, dashboard, etc.)
- Servicio NO mantiene nada en memoria, libera recursos
- Horas o días después, aprobador toma decisión
- Sistema de UI/backoffice emite evento "AprobacionOtorgada" o "AprobacionRechazada"
- Servicio escucha evento, consulta estado de saga en tabla
- Servicio carga contexto de negocio desde tabla
- Servicio reanuda flujo desde el paso siguiente
- Servicio actualiza estado en tabla
Beneficio: El servicio es stateless, puede reiniciarse sin perder progreso.
2. Sub-Sagas Anidadas
Definición: Una saga que, como parte de uno de sus pasos, inicia otra saga completa e independiente.
Ejemplo:
- Saga principal: "ProcesarCompraEmpresarial"
- Paso 3 de la saga requiere: "ValidarCreditoProveedor"
- ValidarCreditoProveedor es en sí una saga con múltiples pasos (consultar bureaus, validar referencias, aprobar monto)
2.1 Reglas de Anidación
Máximo permitido: 2 niveles de profundidad
- Saga Nivel 0 (raíz)
- Saga Nivel 1 (hija directa)
- Prohibido: Saga Nivel 2 (nieta)
Razón: Complejidad exponencial de compensación. Si una saga de nivel 2 falla, hay que compensar nivel 2, luego nivel 1, luego nivel 0. El rastreo se vuelve intratable.
2.2 Responsabilidad de Compensación
Principio: La saga padre es responsable de compensar sagas hijas si el flujo general falla.
Ejemplo:
Saga Padre: ProcesarCompra
- Paso 1: CrearOrden → OK
- Paso 2: ValidarCredito (invoca sub-saga) → OK
- Paso 3: EnviarMercancia → FALLA
Compensación:
- Saga Padre emite evento de compensación para Paso 3 (no aplica, nunca ocurrió)
- Saga Padre emite evento "CancelarValidacionCredito" para sub-saga
- Sub-saga ejecuta su propia compensación interna (liberar límite de crédito reservado)
- Saga Padre compensa Paso 1 (CancelarOrden)
Implementación:
La saga padre debe:
- Mantener registro de todas las sub-sagas iniciadas (almacenar Sub-Saga IDs)
- Al compensar, emitir eventos de compensación dirigidos a cada sub-saga
- Esperar confirmación de compensación de sub-sagas antes de completar su propia compensación
- Implementar timeout: si sub-saga no confirma compensación en X tiempo, alertar para intervención manual
2.3 Propagación de Contexto
Identificadores requeridos:
- Saga ID del padre (Root Saga ID)
- Saga ID de la hija (Child Saga ID)
- Nivel de anidación (0 = raíz, 1 = hija)
Headers en eventos de sub-saga:
- X-Root-Saga-ID: ID de la saga raíz
- X-Parent-Saga-ID: ID de la saga que inició esta
- X-Saga-Level: Nivel numérico de anidación
Uso en observabilidad: Permite visualizar jerarquía completa en herramientas de tracing:
- Root Saga: ProcesarCompra-123
- Child Saga: ValidarCredito-456
- Event: ConsultarBureau
- Event: ValidarReferencias
- Event: EnviarMercancia
- Child Saga: ValidarCredito-456
3. Semantic Lock (Bloqueo Semántico de Negocio)
Problema: Prevenir race conditions en recursos críticos sin recurrir a bloqueos pesimistas de base de datos que reducen throughput.
Ejemplo del problema: Dos usuarios intentan reservar el mismo asiento de avión simultáneamente. Sin bloqueo, ambos podrían ver "asiento disponible" y ambos intentar comprarlo.
3.1 Concepto de Reserva Soft
En lugar de modificar inmediatamente el estado del recurso, se marca como "reservado temporalmente" con:
- Identificador de quién reservó (Saga ID)
- Timestamp de expiración (TTL - Time To Live)
Diferencia con bloqueo tradicional:
- Bloqueo tradicional (SELECT FOR UPDATE): Mantiene lock de base de datos hasta commit/rollback
- Semantic Lock: Marca lógica en el dato que otros respetan, lock se libera automáticamente por TTL
3.2 Flujo de Tres Fases
Fase 1 - Reserva Soft (Check):
- Servicio verifica disponibilidad del recurso
- Si está disponible, marca como "reservado" con Saga ID y TTL de 5 minutos
- Emite evento "RecursoReservado"
- NO compromete definitivamente el recurso
Fase 2 - Operación Crítica (Execute):
- Se ejecuta la operación costosa (ej: cobro de pago)
- Si falla, la reserva expira automáticamente por TTL
- Si tiene éxito, emite evento para confirmar
Fase 3 - Confirmación Hard (Commit):
- Servicio escucha evento de éxito de operación crítica
- Convierte reserva soft en asignación definitiva
- Cambia estado de "reservado" a "vendido"
- Limpia TTL
Fase Alternativa - Liberación Automática:
- Si saga falla o expira, el TTL llega a cero
- Job periódico (cada minuto) busca reservas expiradas
- Cambia estado de "reservado" a "disponible"
- Recurso queda libre para otros
3.3 Ejemplo Completo: Reserva de Asiento
Tabla de asientos:
Campos:
- id_asiento (PK)
- numero_asiento
- estado: DISPONIBLE, RESERVADO, VENDIDO
- reservado_por_saga_id (nullable)
- reservado_hasta_timestamp (nullable)
Paso 1 - Validación y Reserva:
Servicio de Asientos escucha OrdenDeVueloSolicitada:
- Consultar: SELECT estado, reservado_hasta FROM asientos WHERE numero = '12A'
- Si estado = VENDIDO: emitir AsientoNoDisponible
- Si estado = RESERVADO AND reservado_hasta > NOW: emitir AsientoNoDisponible
- Si estado = DISPONIBLE OR (estado = RESERVADO AND reservado_hasta <= NOW):
- UPDATE asientos SET estado = 'RESERVADO', reservado_por_saga_id = 'saga-123', reservado_hasta = NOW + 5 minutos
- Emitir AsientoReservado
- COMMIT
Paso 2 - Pago:
Servicio de Pagos procesa cobro (toma 30 segundos):
- Si éxito: emite PagoExitoso
- Si fallo: NO emite nada, saga expira
Paso 3 - Confirmación:
Servicio de Asientos escucha PagoExitoso:
- Verificar que saga_id coincide: SELECT reservado_por_saga_id FROM asientos WHERE numero = '12A'
- UPDATE asientos SET estado = 'VENDIDO', reservado_por_saga_id = NULL, reservado_hasta = NULL
- Emitir AsientoConfirmado
- COMMIT
Job de Limpieza (cada 1 minuto):
- SELECT numero FROM asientos WHERE estado = 'RESERVADO' AND reservado_hasta <= NOW
- Para cada asiento encontrado:
- UPDATE asientos SET estado = 'DISPONIBLE', reservado_por_saga_id = NULL, reservado_hasta = NULL
- Registrar en log: "Reserva expirada para asiento X de saga Y"
Ventajas de este patrón:
- Alta concurrencia: No bloquea filas durante el pago
- Auto-recuperación: Fallos liberan recursos automáticamente
- Fairness: Primer solicitante obtiene reserva temporal
- Sin deadlocks: No hay bloqueos de base de datos
Desventajas:
- Complejidad adicional en lógica de negocio
- Requiere job de limpieza confiable
- Ventana de race condition muy pequeña (pero existe) en actualización de reserva
PARTE IV: GUÍAS DE IMPLEMENTACIÓN
1. Checklist de Desarrollo
Todo servicio que participe en una saga debe cumplir los siguientes requisitos antes de pasar a producción:
1.1 Persistencia y Mensajería
Verificar que:
- Existe tabla OUTBOX con todos los campos mandatorios
- Existe tabla de mensajes procesados para deduplicación
- Existe proceso Relay que publica eventos desde OUTBOX al broker
- El Relay se ejecuta con intervalo no mayor a 1 segundo
- Todos los eventos incluyen campo schema_version
- Todos los eventos tienen esquemas registrados en Schema Registry
1.2 Idempotencia
Verificar que:
- Todo handler de evento verifica Message ID antes de procesar
- La verificación y el procesamiento están en la misma transacción
- Existe test automatizado que envía mismo mensaje 3 veces y verifica resultado único
- Existe limpieza automática de tabla de mensajes procesados
1.3 Compensación
Verificar que:
- Para cada operación de escritura existe handler de compensación
- La compensación es idempotente (puede ejecutarse N veces)
- Existen tests que verifican que compensación revierte el estado
- La compensación emite evento de confirmación
- La compensación se registra en logs de auditoría
1.4 Configuración
Verificar que:
- Todos los parámetros de timeout son configurables externamente
- Valores por defecto cumplen con mínimos del estándar
- Configuración se carga al inicio y se valida
- Cambios de configuración no requieren recompilación
1.5 Observabilidad
Verificar que:
- Todos los logs de saga son estructurados (no texto plano)
- Saga ID se propaga en todos los eventos emitidos
- Todas las métricas mandatorias están implementadas
- Existe dashboard de monitoreo con visualización de métricas
- Existen alertas configuradas para casos anómalos
1.6 Testing
Verificar que:
- Existen tests de happy path completo
- Existen tests de cada escenario de compensación
- Existen tests de race conditions simuladas
- Existen tests de chaos engineering (matar servicio en medio de saga)
- Existe test de duplicación de mensaje
2. Templates de Eventos Estándar
Todos los eventos deben seguir una estructura consistente para facilitar consumo y rastreo.
2.1 Estructura Base de Evento
Todo evento publicado debe incluir tres secciones:
Sección 1 - Metadatos de Rastreo:
- Identificador único del mensaje (UUID)
- Identificador de la saga (UUID)
- Identificador del span actual (UUID)
- Identificador del span padre (UUID o null si es raíz)
- Versión del esquema del evento (formato semántico)
- Timestamp de creación en UTC ISO-8601
- Nombre del servicio que emitió el evento
- Tipo de evento (nombre descriptivo)
Sección 2 - Datos de Negocio (Payload):
- Información específica del dominio
- Solo datos necesarios para los consumidores
- Sin información sensible sin encriptar
- Con tipos de datos explícitos
Sección 3 - Metadatos Adicionales (Opcional):
- Identificador de correlación de negocio
- Identificador del usuario que inició el flujo
- Información de contexto relevante
- Tags o labels para filtrado
2.2 Ejemplo Descriptivo: Evento OrdenSolicitada
Metadatos de rastreo:
- message_id contiene un UUID único generado al crear el evento
- saga_id contiene el identificador de la saga completa de procesamiento de orden
- span_id contiene un UUID único para este evento específico
- parent_span_id contiene null porque este es el evento inicial
- schema_version contiene el string "1.0.0"
- timestamp contiene la fecha y hora de creación en formato ISO-8601 UTC
- source_service contiene el string "pedidos"
- event_type contiene el string "OrdenSolicitada"
Payload de negocio:
- order_id contiene el identificador único de la orden en el sistema de pedidos
- customer_id contiene el identificador del cliente que hizo la orden
- items es una lista de objetos, cada uno contiene:
- sku: código del producto
- quantity: cantidad solicitada (número entero)
- price: precio unitario (número decimal con dos decimales)
- total_amount contiene el monto total de la orden (número decimal)
- shipping_address es un objeto que contiene:
- street: calle
- city: ciudad
- country: país
- postal_code: código postal
- payment_method es un objeto que contiene:
- type: tipo de pago (string: "credit_card", "paypal", etc.)
- token: token de pago tokenizado (NO el número de tarjeta real)
Metadatos adicionales:
- correlation_id contiene un identificador de correlación de negocio (ej: número de orden visible al cliente)
- user_id contiene el identificador del usuario autenticado
- channel contiene el canal de origen: "web", "mobile", "api"
3. Estrategias de Testing
3.1 Tests de Unidad para Idempotencia
Objetivo: Verificar que procesar el mismo evento múltiples veces produce el mismo resultado.
Escenario de test:
- Preparar estado inicial de base de datos
- Crear un evento de prueba con Message ID específico
- Ejecutar handler del evento por primera vez
- Capturar estado resultante de base de datos
- Ejecutar handler del mismo evento (mismo Message ID) por segunda vez
- Verificar que estado de base de datos es idéntico al del paso 4
- Ejecutar handler por tercera vez
- Verificar nuevamente estado idéntico
- Verificar que tabla de mensajes procesados tiene solo UNA entrada para ese Message ID
Resultado esperado:
- Operación de negocio se ejecutó solo una vez
- Llamadas subsecuentes fueron ignoradas
- No hubo duplicación de datos
- No hubo errores
3.2 Tests de Compensación
Objetivo: Verificar que la transacción compensatoria revierte el efecto de la operación original.
Escenario de test para Inventario:
- Estado inicial: Producto SKU-999 tiene 100 unidades
- Publicar evento PagoExitoso para orden de 5 unidades de SKU-999
- Esperar procesamiento
- Verificar: Producto SKU-999 tiene 95 unidades
- Publicar evento FalloAsignacion (trigger de compensación)
- Esperar procesamiento de compensación
- Verificar: Producto SKU-999 tiene 100 unidades nuevamente
- Verificar en logs: Existe entrada de compensación ejecutada
- Verificar: Se emitió evento StockLiberado
Casos adicionales a probar:
- Compensar cuando la operación original nunca ocurrió (compensación debe ser no-op)
- Compensar dos veces (debe ser idempotente)
- Compensar cuando el recurso ya no existe (debe manejarse gracefully)
3.3 Tests de Chaos Engineering
Objetivo: Verificar resiliencia ante fallos de infraestructura.
Escenario 1 - Matar Servicio Después de COMMIT:
- Iniciar saga de prueba
- Instrumentar código para matar proceso inmediatamente después de COMMIT en base de datos pero ANTES de enviar ACK al broker
- Iniciar servicio
- Esperar que saga procese
- Servicio muere
- Verificar: Cambio en BD se persistió
- Verificar: Evento en OUTBOX se persistió
- Reiniciar servicio
- Verificar: Relay publica evento pendiente
- Verificar: Saga continúa normalmente
Escenario 2 - Desconectar Broker Durante Saga:
- Iniciar saga
- Cuando llegue al paso 2, simular desconexión de red al broker
- Verificar: Servicio reintenta con exponential backoff
- Verificar: Eventos quedan pendientes en OUTBOX
- Restaurar conexión después de 2 minutos
- Verificar: Relay publica eventos pendientes
- Verificar: Saga completa exitosamente
Escenario 3 - Latencia Extrema de Base de Datos:
- Simular latencia de 10 segundos en todas las queries de BD
- Iniciar saga
- Verificar: Timeouts se activan correctamente
- Verificar: Mensajes van a DLQ después de reintentos
- Verificar: Se emiten alertas apropiadas
- Verificar: No hay deadlocks ni procesos zombies
3.4 Tests End-to-End de Saga Completa
Objetivo: Verificar flujo completo en ambiente similar a producción.
Infraestructura de test:
- Broker real (Kafka o RabbitMQ dockerizado)
- Bases de datos reales por servicio (PostgreSQL dockerizado)
- Los tres servicios corriendo (Pedidos, Inventario, Pagos)
- Mock de pasarela de pagos externa
Test de Happy Path:
- Insertar datos de prueba en BD de Inventario: SKU-TEST con 50 unidades
- Enviar request HTTP POST a servicio de Pedidos: crear orden de 10 unidades de SKU-TEST
- Esperar máximo 10 segundos
- Verificar en BD de Pedidos: orden existe con estado COMPLETADA
- Verificar en BD de Inventario: SKU-TEST tiene 40 unidades
- Verificar en BD de Pagos: existe registro de transacción aprobada
- Verificar en logs: todos los eventos se emitieron en orden correcto
- Verificar en métricas: saga_duration_seconds registró tiempo total
Test de Fallo y Compensación:
- Configurar mock de pasarela para rechazar pagos
- Insertar datos: SKU-TEST2 con 20 unidades
- Enviar request: crear orden de 5 unidades de SKU-TEST2
- Esperar procesamiento
- Verificar: Orden en estado CANCELADA_PAGO_RECHAZADO
- Verificar: Inventario NO se modificó (sigue en 20 unidades)
- Verificar: NO existe registro de transacción en BD de Pagos
- Verificar en logs: evento PagoRechazado fue emitido
Test de Race Condition:
- Insertar datos: SKU-TEST3 con 1 unidad
- Lanzar DOS requests simultáneos: ambos piden 1 unidad de SKU-TEST3
- Esperar procesamiento
- Verificar: UNA orden en estado COMPLETADA
- Verificar: OTRA orden en estado CANCELADA_CON_REEMBOLSO
- Verificar: Inventario en 0 unidades (solo una asignación exitosa)
- Verificar en logs: evento FalloAsignacion emitido para segunda saga
- Verificar: Mock de pasarela recibió llamada de reembolso
ANEXO A: Architecture Decision Records (ADR)
ADR-001: Prohibición de Two-Phase Commit y XA Transactions
Fecha: 2026-02-01
Estado: Aceptado
Contexto:
En arquitecturas de microservicios distribuidos, la coordinación de transacciones entre múltiples servicios requiere decisiones sobre consistencia vs disponibilidad. El protocolo Two-Phase Commit (2PC) y las transacciones XA ofrecen atomicidad estricta pero con costos significativos.
Decisión:
Se prohíbe el uso de transacciones distribuidas bloqueantes (2PC, XA Transactions) en todo el ecosistema de microservicios. En su lugar, se adopta el patrón Saga con consistencia eventual.
Razones:
Disponibilidad: 2PC requiere que todos los participantes estén disponibles simultáneamente. Si un servicio está caído, toda la transacción se bloquea. En sistemas distribuidos con múltiples servicios, la probabilidad de que algún componente esté temporalmente no disponible es alta.
Latencia: El protocolo requiere múltiples roundtrips de red (prepare, vote, commit). Esto incrementa significativamente la latencia percibida por usuarios finales.
Bloqueos: Los recursos (filas de base de datos) quedan bloqueados durante todo el protocolo. En sistemas de alta concurrencia, esto reduce dramáticamente el throughput.
Complejidad Operacional: Requiere coordinador transaccional centralizado (Transaction Manager) que se convierte en punto único de fallo. La recuperación de fallos en 2PC es compleja y propensa a estados inconsistentes.
Escalabilidad Limitada: No escala horizontalmente bien porque el coordinador se convierte en bottleneck.
Consecuencias:
Positivas:
- Mayor disponibilidad del sistema (cada servicio puede operar independientemente)
- Mejor throughput en operaciones concurrentes (sin bloqueos prolongados)
- Escalabilidad horizontal sin límites de coordinador central
- Resiliencia ante fallos parciales (saga puede progresar aunque un servicio esté caído temporalmente)
Negativas:
- Complejidad lógica incrementada (implementación de compensaciones)
- Ventanas de inconsistencia temporal (datos pueden estar desincronizados por segundos o minutos)
- Mayor complejidad en testing (necesidad de probar escenarios de compensación)
- Debugging más complejo (flujo implícito, necesidad de rastreo distribuido)
Mitigaciones de Consecuencias Negativas:
- Implementar Transactional Outbox Pattern para garantizar publicación de eventos
- Establecer observabilidad robusta con rastreo distribuido
- Documentar claramente lógica de compensación
- Implementar idempotencia estricta en todos los consumidores
- Definir límites de timeout para evitar sagas infinitas
ADR-002: Validación de Inventario Antes de Pago
Fecha: 2026-02-01
Estado: Aceptado
Contexto:
En sistemas de comercio electrónico, existen dos estrategias principales para manejar inventario en el flujo de compra:
- Estrategia A (Pay-First): Cobrar primero, luego verificar/asignar inventario. Si no hay stock, reembolsar.
- Estrategia B (Check-Then-Pay): Verificar inventario primero, luego cobrar solo si hay disponibilidad.
Decisión:
Se adopta la estrategia Check-Then-Pay: validar disponibilidad de inventario ANTES de ejecutar el cobro financiero.
Razones:
Costos Financieros: Cada transacción con pasarela de pagos tiene un costo (típicamente 2.9% + 0.30 USD). Los reembolsos también incurren en costos. La estrategia Pay-First genera costos innecesarios cuando el pedido finalmente se cancela por falta de stock.
Experiencia de Usuario: Los clientes perciben negativamente ser cobrados y luego reembolsados días después. Genera desconfianza y fricción. La estrategia Check-Then-Pay ofrece feedback inmediato sobre disponibilidad.
Complejidad Contable: Los reembolsos complican la contabilidad y reconciliación bancaria. Requieren procesos adicionales de seguimiento.
Carga en Soporte: Clientes cobrados y luego reembolsados generan tickets de soporte preguntando por el cargo temporal.
Trade-offs Considerados:
Ventana de Race Condition: Entre la validación (Fase 2) y la asignación (Fase 4), el inventario puede ser consumido por otra transacción concurrente. Esto resulta en que algunos pagos exitosos requieran reembolso de todos modos.
Latencia Adicional: Agregar paso de validación aumenta latencia total del flujo en aproximadamente 200-500ms.
Mitigación del Race Condition:
- Implementar re-verificación con bloqueo pesimista en Fase 4
- Implementar compensación automática de pago (reembolso) cuando ocurra race condition
- Monitorear tasa de race conditions y ajustar inventario buffer si es muy alta
Consecuencias:
Positivas:
- Reducción del 95% en costos de transacciones fallidas (según estudios de caso de e-commerce)
- Mejor experiencia de usuario (feedback inmediato de falta de stock)
- Menor carga operacional (menos reembolsos manuales)
- Contabilidad más limpia
Negativas:
- Posibilidad de race condition (aproximadamente 5% de casos en alta concurrencia)
- Latencia adicional de 200-500ms por validación previa
- Complejidad de implementar lógica de re-verificación con bloqueo
Métricas de Éxito:
- Tasa de reembolsos por falta de stock < 5%
- Latencia total de checkout < 3 segundos en percentil 95
- Tasa de quejas de clientes por cobros incorrectos < 0.1%
ADR-003: Coreografía con Coordinador de Estado vs Coreografía Pura
Fecha: 2026-02-01
Estado: Aceptado
Contexto:
Las sagas pueden implementarse mediante dos patrones principales:
Coreografía Pura: Cada servicio escucha eventos relevantes y emite eventos de resultado sin conocer el flujo completo. No existe ningún componente central.
Orquestación: Un servicio centralizado (orquestador) coordina toda la saga invocando directamente a participantes y esperando respuestas.
Híbrido (Coreografía con Coordinador de Estado): Servicios se comunican vía eventos (coreografía) pero uno de ellos mantiene estado de la saga para visibilidad y decisiones de alto nivel.
Decisión:
Se adopta el patrón híbrido: Coreografía con Coordinador de Estado opcional. Se prohíbe orquestación tradicional con llamadas síncronas.
Razones a Favor de Coreografía:
- Bajo acoplamiento entre servicios
- Alta escalabilidad y resiliencia
- Facilita evolución independiente de servicios
- No hay punto único de fallo
Razones Contra Coreografía Pura:
- Difícil rastrear estado completo de una saga
- Complejo identificar en qué punto está una transacción de negocio
- No hay lugar obvio para implementar timeouts de saga completa
- Testing end-to-end más complejo
Razones Contra Orquestación Tradicional:
- Orquestador se convierte en bottleneck
- Alto acoplamiento (orquestador conoce todos los servicios)
- Punto único de fallo
- Dificulta escalabilidad horizontal
Solución Híbrida:
Permitir que el servicio iniciador (ej: Pedidos) actúe como Coordinador de Estado con restricciones:
- Puede mantener tabla de estado de saga
- Puede escuchar eventos de progreso
- Puede tomar decisiones de compensación
- NO puede invocar directamente a otros servicios
- NO puede ejecutar lógica de negocio de otros dominios
Consecuencias:
Positivas:
- Mantiene beneficios de coreografía (bajo acoplamiento, escalabilidad)
- Provee visibilidad centralizada de estado de saga
- Facilita implementación de timeouts
- Simplifica queries de estado para usuarios
- Facilita testing y debugging
Negativas:
- Incremento leve de complejidad en servicio coordinador
- Riesgo de que coordinador acumule lógica que debería estar en otros servicios (debe vigilarse en code reviews)
Reglas de Implementación:
- El coordinador solo mantiene estado, no ejecuta lógica de negocio
- Toda comunicación sigue siendo asíncrona vía eventos
- El coordinador es stateless (estado persiste en BD, no en memoria)
- Debe ser posible eliminar el coordinador sin romper el flujo (otros servicios siguen funcionando)
ADR-004: Outbox Pattern como Estándar Obligatorio
Fecha: 2026-02-01
Estado: Aceptado
Contexto:
Al trabajar con microservicios y messaging, existe el problema de Dual Writes: un servicio necesita actualizar su base de datos local Y publicar un evento en el broker. No existe transacción distribuida que abarque ambos sistemas.
Escenarios problemáticos sin Outbox:
- Escenario 1: Servicio actualiza BD, luego intenta publicar en broker, pero broker está caído → Cambio persiste pero evento nunca se publica → Inconsistencia
- Escenario 2: Servicio publica en broker exitosamente, luego intenta commit en BD, pero falla → Evento publicado pero cambio no persiste → Inconsistencia
- Escenario 3: Servicio hace commit en BD, luego proceso muere antes de publicar → Evento perdido → Inconsistencia
Decisión:
Hacer obligatorio el uso de Transactional Outbox Pattern en todos los servicios que participen en sagas.
Razones:
Atomicidad Garantizada: La tabla OUTBOX está en la misma base de datos que las tablas de negocio. Un commit atómico garantiza que ambos (cambio de negocio + evento) se persistan juntos o ninguno se persista.
Resiliencia ante Fallos: Si el proceso muere después del commit pero antes de publicar, el Relay independiente eventualmente publicará el evento pendiente.
Simplicidad Conceptual: La lógica de negocio se simplifica: solo se preocupa por persistir en BD local. La publicación en broker es responsabilidad del Relay.
Debugging Facilitado: Todos los eventos a publicar quedan registrados en una tabla. Se puede auditar qué eventos se publicaron, cuándo, cuántos intentos tomó, etc.
Implementaciones Consideradas:
Opción A - Polling: Proceso que consulta tabla OUTBOX periódicamente
- Pros: Simple de implementar, funciona con cualquier BD
- Contras: Latencia adicional (según intervalo de polling)
Opción B - Change Data Capture (CDC): Herramienta lee transaction log de BD
- Pros: Latencia mínima (casi real-time), no impacta rendimiento de BD
- Contras: Requiere herramienta adicional (Debezium), complejidad operacional
Opción C - Triggers de BD: Trigger que publica al insertar en OUTBOX
- Pros: Latencia cero
- Contras: Acopla BD con broker, dificulta testing, problemas de resiliencia
Decisión de Implementación:
- Opción A (Polling) es mandatoria para todos los servicios
- Opción B (CDC) es recomendada para servicios críticos de alto volumen
- Opción C (Triggers) está prohibida por acoplamiento
Consecuencias:
Positivas:
- Cero pérdida de eventos
- Garantía de atomicidad entre cambio de negocio y publicación
- Resiliencia ante fallos de broker o red
- Auditoría completa de eventos
Negativas:
- Latencia adicional (100-500ms típicamente con polling)
- Necesidad de proceso Relay adicional
- Tabla OUTBOX crece y requiere limpieza
- Complejidad adicional en infraestructura
Mitigaciones:
- Optimizar intervalo de polling (100-500ms es aceptable)
- Implementar limpieza automática de eventos publicados después de 7 días
- Usar índices apropiados en tabla OUTBOX para queries eficientes
- Monitorear tamaño de OUTBOX y alertar si crece anormalmente
ANEXO B: Glosario de Términos
BASE: Modelo de consistencia para sistemas distribuidos. Acrónimo de Basically Available (Básicamente Disponible), Soft state (Estado Suave), Eventually consistent (Eventualmente Consistente). Contrasta con ACID.
Broker de Mensajes: Sistema intermediario que facilita comunicación asíncrona entre servicios mediante colas y topics. Ejemplos: Kafka, RabbitMQ.
Change Data Capture (CDC): Técnica para detectar y capturar cambios en base de datos mediante lectura del transaction log. Usado para publicar eventos sin Dual Write Problem.
Compensación: Transacción lógicamente inversa que deshace o mitiga el efecto de una operación previamente confirmada. Ejemplo: Si se cargó una tarjeta, la compensación es reembolsar.
Consistencia Eventual: Propiedad de sistemas distribuidos donde, en ausencia de nuevas actualizaciones, eventualmente todas las réplicas convergerán al mismo estado. No garantiza cuándo ocurrirá.
Coreografía: Patrón de saga donde cada servicio escucha eventos relevantes y reacciona autónomamente sin coordinación central. Comparable a bailarines que siguen música sin director.
Correlation ID: Identificador único que se propaga a través de múltiples servicios y operaciones para rastrear una transacción de negocio completa en logs y métricas distribuidos.
Dead Letter Queue (DLQ): Cola especial donde se envían mensajes que fallaron repetidamente después de múltiples intentos de procesamiento. Permite análisis y reprocesamiento manual.
Dual Write Problem: Problema de consistencia que ocurre al intentar escribir en dos sistemas diferentes (ej: base de datos + message broker) sin transacción atómica que abarque ambos.
Exponential Backoff: Estrategia de reintentos donde el tiempo de espera entre intentos crece exponencialmente. Ejemplo: 1s, 2s, 4s, 8s, 16s. Previene sobrecarga durante fallos.
Idempotencia: Propiedad de una operación que puede ejecutarse múltiples veces sin cambiar el resultado más allá de la primera ejecución. Ejemplo: "Establecer X = 5" es idempotente, "Incrementar X" no lo es.
Message Broker: Ver Broker de Mensajes.
Orquestación: Patrón de saga donde un componente central (orquestador) coordina explícitamente todos los pasos invocando a participantes y esperando respuestas.
Outbox Pattern: Ver Transactional Outbox Pattern.
Race Condition: Situación donde el resultado de una operación depende del tiempo relativo de eventos concurrentes. Ejemplo: dos transacciones leyendo el mismo inventario antes de que cualquiera lo actualice.
Relay: Proceso que lee eventos de la tabla OUTBOX y los publica en el message broker. Puede ser implementado via polling o CDC.
Saga: Patrón de diseño para manejar transacciones distribuidas mediante secuencia de transacciones locales coordinadas por eventos, con compensaciones para revertir en caso de fallo.
Schema Registry: Servicio centralizado que almacena y versiona esquemas de eventos/mensajes. Permite validación de compatibilidad y generación de documentación.
Semantic Lock: Bloqueo lógico a nivel de negocio (no de base de datos) que marca un recurso como "reservado temporalmente" con TTL, permitiendo alta concurrencia.
Span: En rastreo distribuido, representa una unidad de trabajo individual. Una saga completa contiene múltiples spans (uno por cada paso/evento).
Timeout: Límite de tiempo máximo para esperar una respuesta o completar una operación. Previene esperas infinitas ante fallos.
Transactional Outbox Pattern: Patrón que resuelve Dual Write Problem insertando eventos en tabla local (OUTBOX) dentro de la misma transacción de negocio, para luego publicarlos asíncronamente.
TTL (Time To Live): Tiempo de vida de un recurso o dato después del cual expira automáticamente. Usado en Semantic Locks para liberar reservas no confirmadas.
Two-Phase Commit (2PC): Protocolo de transacciones distribuidas bloqueantes que garantiza atomicidad mediante fase de preparación y fase de commit. Prohibido en este estándar.
ANEXO C: Referencias y Recursos
Documentación Técnica Recomendada
Libros:
- "Designing Data-Intensive Applications" por Martin Kleppmann - Capítulos 7-9 sobre transacciones distribuidas y consistencia
- "Microservices Patterns" por Chris Richardson - Capítulo 4 completo sobre Sagas
- "Building Microservices" por Sam Newman - Segunda edición, capítulo sobre workflows y consistencia
Papers Académicos:
- "Sagas" por Hector Garcia-Molina y Kenneth Salem (1987) - Paper original que define el patrón
- "Life beyond Distributed Transactions: an Apostate's Opinion" por Pat Helland - Argumentos contra transacciones distribuidas
- "Building on Quicksand" por Pat Helland y Dave Campbell - Fundamentos de consistencia eventual
Recursos Online:
- Microservices.io - Patrón Saga: https://microservices.io/patterns/data/saga.html
- Documentación de Debezium para CDC: https://debezium.io
- Confluent Schema Registry documentation: https://docs.confluent.io/platform/current/schema-registry/
Bibliotecas y Frameworks Recomendados
Para Java/JVM:
- Eventuate Tram Saga Framework - Framework especializado en sagas con outbox pattern
- Axon Framework - CQRS y Event Sourcing con soporte para sagas
- Apache Camel - Integración con múltiples brokers y patrones de mensajería
Para .NET:
- MassTransit - Framework de messaging con soporte nativo para sagas
- NServiceBus - Bus de servicios con soporte para sagas de larga duración
- Rebus - Bus de mensajes ligero con soporte para sagas
Para Node.js:
- Moleculer - Framework de microservicios con soporte para sagas
- NestJS con Bull - Soporte para colas y workflows complejos
Para Python:
- Nameko - Framework de microservicios con soporte para eventos
- Celery - Sistema de colas distribuidas con soporte para workflows
Herramientas de Observabilidad
Rastreo Distribuido:
- Jaeger - Sistema open-source de rastreo distribuido
- Zipkin - Rastreo distribuido con múltiples integraciones
- AWS X-Ray - Servicio administrado de AWS para rastreo
- Google Cloud Trace - Servicio de rastreo para GCP
Logging:
- ELK Stack (Elasticsearch, Logstash, Kibana) - Stack completo de logging
- Grafana Loki - Sistema de agregación de logs
- Splunk - Plataforma enterprise de análisis de logs
Métricas:
- Prometheus - Sistema de métricas con modelo pull
- Grafana - Visualización de métricas
- Datadog - Plataforma SaaS completa de observabilidad
Comunidades y Foros
- CNCF Slack - Canal #microservices
- Stack Overflow - Tag "saga-pattern" y "distributed-transactions"
- Reddit r/microservices
- DDD/CQRS Google Group
FIN DEL DOCUMENTO
Última Actualización: Febrero 2026
Próxima Revisión: Agosto 2026
Responsable: Equipo de Arquitectura Empresarial
Contacto: [email protected]
Arquitectura distribuida y el abandono consciente de ACID
- Mauricio ECR
- Arquitectura
- 14 Feb, 2026
En el mundo de los sistemas distribuidos, hay una verdad incómoda que enfrentamos tarde o temprano: no podemos tenerlo todo. La promesa de las transacciones ACID tradicionales —esa garantía tranquiliz
Arquitectura distribuida y el abandono consciente de ACID
- Mauricio ECR
- Arquitectura
- 14 Feb, 2026
En el mundo de los sistemas distribuidos, hay una verdad incómoda que enfrentamos tarde o temprano: no podemos tenerlo todo. La promesa de las transacciones ACID tradicionales —esa garantía tranquilizadora de que nuestros datos siempre estarán perfectamente sincronizados— se desvanece en el momento en que decidimos distribuir nuestra aplicación monolítica en múltiples servicios independientes. Es un momento de madurez arquitectónica que muchos equipos experimentan con cierta resistencia, similar a cuando un adolescente comprende que el mundo no es tan simple como parecía en la infancia.
Esta transformación no es meramente técnica; representa un cambio filosófico en cómo concebimos la consistencia de datos. Abandonamos el confort de las transacciones atómicas inmediatas, donde todo sucede o nada sucede en un instante perfecto, y abrazamos algo más orgánico, más real: la consistencia eventual. Los datos pueden estar temporalmente desalineados entre servicios, como músicos de una orqestra que momentáneamente pierden el compás para luego reconectarse con la melodía principal. La belleza de este modelo —conocido como BASE (Basically Available, Soft state, Eventually consistent)— radica en su pragmatismo: el sistema responde incluso cuando algunos servicios están caídos, los eventos se persisten antes de considerarse publicados, y eventualmente, con la paciencia de un jardinero que espera la floración, el sistema converge hacia un estado consistente.
El Arte de la Coreografía Distribuida
Imagina un ballet donde los bailarines no siguen a un director de orquesta central, sino que responden a las acciones de sus compañeros de manera autónoma. Este es el corazón del patrón Saga basado en coreografía. Cada servicio actúa como un participante independiente que escucha eventos relevantes de su dominio, ejecuta su lógica de negocio local, publica eventos de resultado y —esto es crucial— no tiene conocimiento completo de qué otros servicios participan en el proceso general.
Esta autonomía trae consigo ventajas significativas: bajo acoplamiento entre servicios, alta escalabilidad y la ausencia de un punto único de fallo. Pero también presenta desafíos genuinos. El flujo completo de la transacción se vuelve implícito, emergente de las interacciones locales, lo que puede hacer que la depuración se asemeje a seguir las huellas de un animal esquivo en el bosque. La complejidad en el rastreo es real, y no debemos minimizarla.
Para contextos donde necesitamos mayor visibilidad del flujo completo, existe una alternativa complementaria: la orquestación ligera de estado. Aquí, un servicio actúa como coordinador de estado —no como un tirano centralizado que comanda cada movimiento, sino como un observador atento que rastrea el progreso, mantiene una tabla de estado de la saga, escucha todos los eventos relevantes y puede tomar decisiones de cancelación cuando sea necesario. La distinción es sutil pero fundamental: el coordinador observa y reacciona, no comanda y espera. La comunicación sigue siendo asíncrona mediante el bus de eventos, preservando así los beneficios de la arquitectura desacoplada.
Hay, por supuesto, caminos que debemos evitar absolutamente. El Two-Phase Commit (2PC) y las transacciones XA, que extienden transacciones de base de datos entre servicios, están prohibidos debido a los bloqueos prolongados que generan y la baja disponibilidad resultante. Los bloqueos distribuidos compartidos entre servicios son igualmente problemáticos, con la excepción de los bloqueos semánticos de negocio que discutiremos más adelante. Las llamadas síncronas en flujos críticos —HTTP, REST, gRPC— deben reservarse exclusivamente para consultas de solo lectura, nunca para comandos que modifican estado en el contexto de una saga.
El Problema Dual Write y su Solución Elegante
Uno de los desafíos más insidiosos en sistemas distribuidos es el "Dual Write Problem". El escenario es simple pero traicionero: un servicio necesita actualizar su base de datos local y publicar un evento en el broker de mensajería. Si la publicación falla después del commit de la base de datos, el sistema queda inconsistente. Si falla antes, perdemos el evento y el flujo se interrumpe sin que nadie lo sepa.
La solución a este dilema es el patrón Transactional Outbox, una técnica elegante que convierte un problema de coordinación distribuida en dos problemas locales secuenciales. La regla de oro es simple: un servicio nunca debe publicar directamente en el broker dentro de su código de negocio. En su lugar, la operación se divide en dos fases distintas.
La primera fase es una transacción atómica local donde todo sucede dentro de los límites seguros de una sola base de datos. Iniciamos la transacción, ejecutamos nuestra operación de negocio —quizás un INSERT, UPDATE o DELETE en las tablas de dominio— e inmediatamente después insertamos el evento que queremos publicar en una tabla especial llamada OUTBOX. Finalmente, confirmamos la transacción completa. El punto crítico aquí es que ambas escrituras están protegidas por el mismo commit atómico: o ambas suceden, o ninguna sucede.
La segunda fase es completamente asíncrona y resiliente. Un proceso independiente —típicamente llamado Relay o Publisher— lee continuamente la tabla OUTBOX, encuentra eventos pendientes, los publica en el broker y los marca como publicados o los elimina. Este proceso puede fallar, reintentar, detenerse y reiniciarse sin comprometer la integridad de nuestros datos, porque la fuente de verdad —la tabla OUTBOX— está segura en nuestra base de datos.
La estructura de la tabla OUTBOX debe incluir campos obligatorios que garanticen su funcionamiento correcto: un identificador único del mensaje (típicamente un UUID), el tipo de evento de dominio, el cuerpo del evento serializado, timestamp de creación, estado de publicación (pendiente, publicado, fallido), número de intentos, el agregado raíz asociado para ordenamiento, y la versión del esquema del evento.
Para implementar el proceso de relay tenemos tres opciones principales. El polling tradicional es simple y confiable: un proceso consulta periódicamente la tabla OUTBOX —recomendamos intervalos de 100 a 500 milisegundos— publica eventos pendientes ordenados por timestamp y los marca como publicados tras confirmación del broker. Change Data Capture (CDC) es más sofisticado: herramientas como Debezium, Maxwell o AWS DMS leen el transaction log de la base de datos, detectan inserts en OUTBOX en tiempo real y publican automáticamente en el broker. Los triggers de base de datos son la opción menos recomendada debido al acoplamiento que generan y su menor resiliencia, aunque técnicamente funcional: un trigger se activa al insertar en OUTBOX e invoca un procedimiento que publica en el broker.
Idempotencia: El Escudo Contra la Duplicación
Aquí nos encontramos con otra verdad incómoda de los sistemas distribuidos: los brokers de mensajería garantizan entrega "al menos una vez", lo que significa que inevitablemente algunos mensajes llegarán duplicados. No es un error del broker, es una característica inherente a sistemas que priorizan la disponibilidad sobre la consistencia perfecta. Por tanto, todo consumidor de eventos debe ser idempotente.
La definición es directa pero profunda: una operación es idempotente si ejecutarla múltiples veces produce el mismo resultado que ejecutarla una sola vez. Es como presionar el botón del elevador repetidamente —el elevador viene una sola vez, sin importar cuántas veces presionamos el botón. Cada servicio debe mantener una tabla dedicada de mensajes procesados con el identificador del mensaje como clave primaria, el tipo de evento procesado, timestamp de procesamiento, estado final (éxito o fallo) y un índice en timestamp para limpieza periódica.
El flujo de procesamiento idempotente se convierte en un ritual casi ceremonial. Al recibir un mensaje del broker, iniciamos una transacción de base de datos local e intentamos insertar el identificador del mensaje en la tabla de registro. Si la inserción falla por clave duplicada, sabemos que este mensaje ya fue procesado anteriormente: hacemos rollback y retornamos éxito al broker sin ejecutar nada. Si la inserción es exitosa, procedemos con la lógica de negocio, potencialmente insertamos un evento resultante en nuestra tabla OUTBOX, confirmamos la transacción completa y enviamos ACK al broker. Como medida de higiene, eliminamos registros con más de siete días de antigüedad mediante un proceso nocturno.
La Danza de la Compensación
Cuando las cosas van mal en un sistema distribuido —y eventualmente lo harán— necesitamos una estrategia para deshacer operaciones que ya fueron confirmadas. Aquí entra el concepto de transacción compensatoria: una acción lógicamente inversa que deshace o mitiga el efecto de una operación previamente confirmada.
Los ejemplos son intuitivos una vez que comprendemos el patrón. CrearPedido se compensa con AnularPedido. ReservarInventario con LiberarReserva. CobrarPago con ReembolsarPago. EnviarNotificacion con EnviarNotificacionCorreccion. AsignarRecurso con DesasignarRecurso. Pero la compensación no siempre es perfecta en el sentido matemático de restaurar el estado exacto anterior.
Existen tres tipos de compensación, cada uno con su propia filosofía. La compensación perfecta restaura el estado exacto anterior —como cancelar una reserva que aún no ha sido utilizada. La compensación aproximada restaura un estado equivalente pero no idéntico —como ofrecer un reembolso en créditos de la tienda en lugar de dinero, cuando ambos tienen valor similar para el cliente. La compensación simbólica es la más interesante: registra el intento de reversión cuando la compensación real es físicamente imposible. No podemos "des-enviar" un email una vez que salió, pero podemos enviar un email de corrección o aclaración.
Las compensaciones deben ser idempotentes (pueden ejecutarse múltiples veces sin efectos adversos), deben registrarse en logs de auditoría para trazabilidad, y deben emitir eventos de compensación para que otros servicios puedan reaccionar apropiadamente.
El Arte del Reintento Inteligente
No todos los errores son creados iguales. Esta distinción es fundamental para implementar una estrategia de reintentos efectiva. Los fallos transitorios son errores temporales que pueden resolverse reintentando: pérdida de conexión de red, timeouts de base de datos por carga momentánea, un servicio dependiente temporalmente no disponible, o límites de rate limiting alcanzados. Los fallos permanentes no se resolverán sin intervención humana o cambios en el código: validaciones de negocio fallidas, datos malformados o incompletos, violaciones de reglas de dominio, permisos insuficientes, o recursos no encontrados.
Para fallos transitorios implementamos exponential backoff con jitter. La progresión es elegante: empezamos con una espera inicial de 500 milisegundos entre el primer y segundo intento, luego multiplicamos por un factor de 2 en cada intento subsecuente, con un techo máximo de 60 segundos entre intentos y un límite de 5 intentos totales. Añadimos jitter aleatorio —una variación del 10-25%— para evitar el fenómeno de "thundering herd" donde múltiples procesos reintentan simultáneamente, creando picos de carga que agravan el problema original. La progresión típica sería: 500ms → 1s → 2s → 4s → 8s → Dead Letter Queue.
La Dead Letter Queue (DLQ) es nuestro hospital para mensajes enfermos. Después de agotar los reintentos automáticos, el mensaje se envía a esta cola especial donde espera análisis manual posterior, genera alertas al equipo de operaciones, puede ser reprocesado manualmente tras corrección del problema subyacente, y sirve para auditoría de fallos recurrentes. Los mensajes en DLQ deben preservar información forense completa: el mensaje original completo, número de intentos realizados, timestamps de cada intento, detalles de cada error ocurrido, y el trace completo del último error.
Evolución sin Ruptura: El Versionado de Contratos
Los sistemas vivos evolucionan, y los contratos entre servicios deben evolucionar con ellos sin romper el ecosistema existente. Todo evento de dominio publicado debe tener un esquema formal —Avro, Protocol Buffers o JSON Schema— que defina nombre y tipo de cada campo, campos obligatorios versus opcionales, tipos de datos permitidos, restricciones de validación, y descripción semántica de cada campo.
El versionado semántico se convierte en nuestra brújula. Cada evento incluye un campo "schema_version" con formato MAJOR.MINOR.PATCH. Un cambio MAJOR indica incompatibilidad que requiere actualización del consumidor: eliminar campos, cambiar tipos de datos existentes, cambiar la semántica de un campo, o renombrar campos. Un cambio MINOR representa adiciones retrocompatibles: agregar nuevos campos opcionales, agregar nuevos valores a enumeraciones, o deprecar campos sin eliminarlos. Un PATCH es para correcciones menores sin impacto funcional: corregir descripciones, mejorar documentación, o corregir typos en nombres.
Para cambios aditivos (Minor o Patch), agregamos solo campos opcionales con valores por defecto. Los consumidores antiguos ignoran campos nuevos que no conocen, los productores nuevos toleran consumidores antiguos, y no se requiere coordinación de despliegue. Es evolución pacífica y gradual.
Para cambios breaking (Major), necesitamos una estrategia más cuidadosa. Creamos un nuevo tipo de evento con sufijo de versión —por ejemplo, "OrdenSolicitada_v2"— y mantenemos publicación dual por un período de transición. El productor emite tanto el evento v1 como el v2, permitiendo que los consumidores migren gradualmente. El período mínimo de convivencia es 90 días calendario. Solo después de este período podemos deprecar y eventualmente eliminar la versión antigua.
La política de deprecación es deliberadamente conservadora: anunciamos la deprecación con 90 días de anticipación, añadimos warnings en logs cuando se use la versión antigua, publicamos métricas de uso de versiones obsoletas, coordinamos la migración con todos los equipos consumidores, y eliminamos soporte solo cuando el uso sea cero por 30 días consecutivos. Es un proceso tedioso, sí, pero necesario para la salud del ecosistema.
Observabilidad: Iluminando la Caja Negra
Un sistema distribuido sin observabilidad es como navegar un barco en niebla densa sin instrumentos. El rastreo distribuido nos permite seguir el viaje completo de una transacción a través de múltiples servicios. El servicio que inicia una saga genera un Saga ID —un identificador global único, típicamente UUID versión 4— que representa toda la transacción distribuida. Este ID se genera una sola vez al inicio y se propaga sin cambios a través de todos los pasos subsecuentes.
Adicionalmente, cada servicio que procesa genera su propio Span ID —un identificador único para ese paso específico— y mantiene referencia al Parent Span ID del paso anterior, creando así una jerarquía de trazas como un árbol genealógico de operaciones. Estos identificadores viajan como headers en todos los mensajes: "X-Saga-ID", "X-Span-ID" y "X-Parent-Span-ID". Los servicios intermedios preservan el Saga ID sin modificarlo, generan su propio Span ID, copian el Span ID recibido como su Parent Span ID, y propagan estos tres valores en todos los eventos que emitan.
El logging estructurado es nuestra memoria colectiva. Cada entrada de log relacionada con procesamiento de eventos debe incluir campos mandatorios de contexto —identificador de saga, tipo de evento, versión del esquema, nombre del servicio, timestamp en ISO-8601 con zona horaria UTC, identificador del span actual y padre— junto con campos mandatorios de resultado: estado del procesamiento (RECEIVED, PROCESSING, SUCCESS, FAILED, COMPENSATING, COMPENSATED), duración en milisegundos de la operación, y número de intento para reintentos.
Las métricas son nuestros sensores vitales. saga_duration_seconds es un histograma que mide el tiempo total desde inicio hasta conclusión, etiquetado por tipo de saga y estado final. saga_step_errors_total es un contador acumulativo de fallos al procesar eventos, etiquetado por nombre de servicio, tipo de evento y tipo de error. saga_compensations_total cuenta las transacciones compensatorias ejecutadas. outbox_pending_messages es un gauge que muestra cuántos eventos están pendientes de publicar en cada servicio. dlq_messages_total indica la cantidad de mensajes problemáticos en Dead Letter Queue.
Para procesos críticos de negocio mantenemos tablas de auditoría completas con todos los cambios de estado de la saga, timestamp de cada transición, razón del cambio (evento que lo provocó), usuario o sistema responsable del inicio, y datos relevantes de negocio. La retención mínima sigue políticas regulatorias, típicamente siete años para sectores financieros.
Límites Temporales y la Paciencia del Sistema
Todo proceso distribuido debe tener límites temporales claramente definidos. No podemos esperar indefinidamente. Los parámetros configurables son nuestra red de seguridad: número máximo de intentos antes de enviar a DLQ (recomendamos 5 intentos con mínimo de 3), espera inicial entre primer y segundo intento (recomendamos 500 milisegundos con mínimo de 100), espera máxima entre intentos (recomendamos 60 segundos con mínimo de 30), timeout de saga completa (recomendamos 24 horas con mínimo de 1 hora, ajustable según naturaleza del proceso), y timeout por paso individual (recomendamos 5 minutos con mínimo de 30 segundos).
La filosofía de timeouts tiene dos niveles. Para timeouts de paso individual, si un evento esperado no llega dentro del plazo configurado, el coordinador emite un evento de timeout, inicia proceso de compensación, registra en logs con nivel ERROR e incrementa métricas de timeouts. Para timeouts de saga completa, si la saga no se completa dentro del plazo total, ejecutamos compensación automática de todos los pasos confirmados, marcamos la saga con estado TIMED_OUT, emitimos evento de saga expirada para auditoría, notificamos al usuario o sistema iniciador del fallo, y generamos alerta para el equipo de operaciones. Crucialmente, no eliminamos datos de auditoría —los mantenemos para análisis post-mortem.
Las alertas obligatorias actúan como sistema de alerta temprana: si el porcentaje de sagas fallidas supera 5% en ventana de 15 minutos, si el tiempo promedio supera el doble del baseline histórico, si la cantidad de mensajes en DLQ supera 10 por servicio, si hay mensajes en OUTBOX pendientes por más de 10 minutos, o si una saga individual supera el 80% del timeout configurado. Cada alerta tiene su nivel de severidad: CRITICAL para flujos de negocio críticos como pagos y pedidos, HIGH para funcionalidad importante pero no crítica, MEDIUM para degradación de rendimiento sin pérdida de funcionalidad, y LOW para anomalías sin impacto inmediato.
Del Concepto a la Realidad: Un Caso Práctico
La teoría cobra vida cuando la aplicamos a un caso concreto. Consideremos un sistema de comercio electrónico donde necesitamos procesar una orden de compra asegurando disponibilidad de inventario antes de ejecutar el cobro. El objetivo es claro: minimizar reembolsos por falta de stock, reducir costos de transacciones bancarias fallidas, y mejorar la experiencia del cliente evitando cobros seguidos de reembolsos inmediatos.
Nuestra estrategia es Check-Then-Act: verificación antes de acción financiera. El proceso se divide en cuatro fases secuenciales, cada una con su propósito específico. En la fase de intención, registramos la orden inicial con estado PENDIENTE_VALIDACION y emitimos el evento OrdenSolicitada. En la fase de validación, consultamos disponibilidad de inventario sin realizar ninguna reserva —es una operación de lectura pura, sin bloqueos— que resulta en StockVerificado o StockNoDisponible. En la fase de cobro condicional, ejecutamos la transacción financiera solo si la fase anterior fue exitosa, resultando en PagoExitoso o PagoRechazado. Finalmente, en la fase de asignación con bloqueo, realizamos el descuento definitivo de inventario con un UPDATE que usa bloqueo pesimista, resultando en StockAsignado o FalloAsignacion.
Tres servicios orquestan este ballet: el Gestor de Pedidos actúa como coordinador de estado, el Gestor de Inventario participa en dos momentos diferentes (validación y asignación), y el Procesador de Pagos actúa como intermediario financiero condicional.
El Gestor de Pedidos mantiene la máquina de estados de la orden. Cuando está en estado PENDIENTE_VALIDACION y recibe StockVerificado, transiciona a PENDIENTE_PAGO. Si recibe StockNoDisponible, va directamente a CANCELADA_SIN_STOCK y el flujo termina. Desde PENDIENTE_PAGO, al recibir PagoExitoso transiciona a PENDIENTE_ASIGNACION, pero si recibe PagoRechazado va a CANCELADA_PAGO_RECHAZADO. Finalmente, desde PENDIENTE_ASIGNACION, StockAsignado lleva a COMPLETADA (el flujo exitoso), mientras que FalloAsignacion resulta en CANCELADA_CON_REEMBOLSO y requiere compensación.
El Gestor de Inventario tiene una doble vida fascinante. En el momento de validación, al escuchar OrdenSolicitada, simplemente consulta disponibilidad actual sin tocar nada. Es como asomarse a la despensa para ver si hay suficiente harina sin tomar nada todavía. Si hay stock suficiente para todos los ítems, emite StockVerificado. Si algún ítem no tiene stock suficiente, emite StockNoDisponible con detalles. En el momento de asignación, al escuchar PagoExitoso, la cosa se pone seria: inicia una transacción con bloqueo pesimista (SELECT FOR UPDATE), re-verifica disponibilidad actual —que puede haber cambiado desde la validación inicial— descuenta las unidades si aún hay stock, y confirma transacción o hace rollback según el resultado.
Esta re-verificación en la fase de asignación es crítica. Durante el tiempo transcurrido entre la validación optimista (fase 2) y la asignación pesimista (fase 4), otra transacción concurrente podría haber consumido ese stock. El bloqueo pesimista en la fase 4 garantiza que, una vez que obtenemos el lock, nadie más puede modificar esos registros hasta que terminemos.
El Procesador de Pagos es deliberadamente ciego a OrdenSolicitada. Solo reacciona a StockVerificado, lo que previene cobros innecesarios cuando no hay stock disponible. Al recibir StockVerificado, valida que el evento no fue procesado previamente (idempotencia), extrae datos de pago, llama a la API de la pasarela con timeout de 30 segundos, y maneja la respuesta apropiadamente. Si la pasarela aprueba, almacena el ID de transacción e inserta PagoExitoso en OUTBOX. Si hay rechazo por fondos insuficientes o tarjeta inválida, inserta PagoRechazado. Si hay timeout o error de red, aplica la política de reintentos con exponential backoff, y tras agotar intentos envía a DLQ para revisión manual.
La compensación en Pagos es igualmente importante. Al recibir FalloAsignacion, extrae el Saga ID para identificar la transacción financiera original, busca en su base de datos local el ID de transacción de la pasarela, llama a la API de reembolso, y si es exitoso inserta ReembolsoEjecutado en OUTBOX. Si el reembolso falla, reintenta con backoff exponencial, y tras 5 intentos fallidos envía alerta crítica al equipo de finanzas y marca para reembolso manual.
Tres Historias, Tres Destinos
El flujo exitoso —el happy path que todos queremos ver— es casi poético en su simplicidad. Un cliente solicita 1 unidad del producto SKU-123. El inventario actual tiene 10 unidades disponibles. No hay operaciones concurrentes. El cliente envía su solicitud HTTP POST, Pedidos crea el registro con estado PENDIENTE_VALIDACION, genera un Saga ID único, inserta OrdenSolicitada en OUTBOX. El relay publica el evento. Inventario lo consume, consulta la base de datos, verifica que hay 10 unidades y el pedido requiere solo 1, inserta StockVerificado en OUTBOX. Pagos consume StockVerificado, invoca la API de la pasarela que aprueba el cobro, inserta PagoExitoso. Inventario consume PagoExitoso, inicia transacción con bloqueo pesimista, re-verifica que aún hay stock, ejecuta el UPDATE restando 1 unidad, inserta StockAsignado, confirma la transacción. Pedidos consume StockAsignado, actualiza estado a COMPLETADA, emite OrdenCompletada, y notifica al cliente. Todo el proceso toma aproximadamente 2-5 segundos. Elegante, eficiente, exitoso.
El fallo temprano es igualmente instructivo. Un cliente solicita 5 unidades del producto SKU-456. El inventario actual tiene 0 unidades. Pedidos crea la orden y emite OrdenSolicitada. Inventario consulta, verifica que hay 0 unidades pero el pedido requiere 5, inmediatamente inserta StockNoDisponible en OUTBOX. Pedidos consume este evento, actualiza estado a CANCELADA_SIN_STOCK, emite OrdenCancelada, y notifica al cliente que el producto está agotado. Lo crucial aquí es que el servicio de Pagos nunca se entera de nada —no escucha StockNoDisponible— por tanto no se ejecuta ninguna operación financiera. No hay cargos, no hay reembolsos. La orden se cancela en menos de 1 segundo. El costo financiero es cero. Esta arquitectura evita aproximadamente el 95% de reembolsos comparado con estrategias de "cobrar primero, verificar después".
El fallo tardío —la race condition— es donde el diseño realmente brilla. Cliente A solicita 1 unidad del producto SKU-789. Inventario actual: 1 unidad disponible. Cliente B también solicita 1 unidad del mismo producto simultáneamente. Ambos pasan la validación optimista porque en ese momento había 1 unidad disponible. Ambos son cobrados exitosamente por la pasarela de pagos. Pero en la fase de asignación, solo uno puede ganar.
Digamos que Cliente A llega primero a la fase de asignación. Inventario inicia transacción con SELECT FOR UPDATE, obtiene el lock, lee 1 unidad disponible, valida que 1 >= 1, ejecuta UPDATE restando la unidad, deja el inventario en 0, inserta StockAsignado, confirma. Cliente A recibe su orden completa. Segundos después, Cliente B llega a la fase de asignación. Inventario inicia otra transacción con SELECT FOR UPDATE, obtiene el lock (Cliente A ya liberó el lock al hacer COMMIT), pero ahora lee 0 unidades disponibles, valida que 0 < 1, la validación falla, no ejecuta el UPDATE, pero —esto es crucial— sí confirma la transacción para que el evento FalloAsignacion se publique correctamente.
Pagos consume FalloAsignacion para Cliente B, busca la transacción original que había aprobado, invoca la API de reembolso de la pasarela, recibe confirmación, inserta ReembolsoEjecutado. Pedidos consume este evento, actualiza estado a CANCELADA_CON_REEMBOLSO, y notifica a Cliente B que su pago ha sido reembolsado porque el producto se agotó durante el proceso. Cliente A tiene su orden completada, Cliente B tiene su dinero de vuelta automáticamente, el sistema mantuvo consistencia a pesar de la concurrencia, y crucialmente, no hubo sobreventa (overselling).
Este escenario demuestra por qué la verificación en fase 2 es optimista (sin bloqueo) y la asignación en fase 4 es pesimista (con bloqueo). Si bloqueáramos en la fase de validación, tendríamos alta contención —cada consulta de disponibilidad bloquearía las filas, forzando a otras transacciones a esperar. El bloqueo tardío minimiza la ventana crítica a solo el momento de la asignación definitiva.
Consideraciones Finales de Implementación
El ordenamiento de eventos merece atención especial. Los brokers garantizan orden dentro de una partición, no globalmente. La solución es particionar eventos por Saga ID, asegurando que todos los eventos de una misma saga vayan a la misma partición. Configuramos la clave de partición como el Saga ID. Esto garantiza que eventos de una orden específica se procesen en orden correcto, aunque no importa el orden entre órdenes diferentes —cada orden es independiente.
El manejo de duplicados es un ritual bien definido. Cuando llega un evento PagoExitoso al servicio de Inventario, extraemos el Message ID del header, iniciamos una transacción, e intentamos insertar ese ID en la tabla processed_messages. Si la inserción falla por clave duplicada, sabemos que este mensaje ya fue procesado: hacemos rollback, retornamos ACK al broker sin hacer nada más, y registramos en logs "Evento duplicado ignorado". Si la inserción es exitosa, procedemos con la lógica de asignación de stock, y el COMMIT incluye tanto la nueva fila en processed_messages como el UPDATE de inventario. Ambos cambios o ninguno —atomicidad local garantizada.
La consistencia de OUTBOX es una garantía crítica inviolable: el evento en OUTBOX y el cambio de estado de negocio deben confirmarse en la misma transacción atómica de base de datos. Por ejemplo, cuando Pedidos recibe StockVerificado, en una sola transacción ejecuta UPDATE de la orden cambiando estado a PENDIENTE_PAGO e INSERT en OUTBOX del evento OrdenActualizada, seguido de COMMIT. Si el COMMIT falla, ningún cambio se persiste. Si se confirma, ambos cambios quedan guardados atómicamente. Esta es la base de toda la confiabilidad del sistema.
Los timeouts deben configurarse pensando en las características reales de cada operación. Para Pedidos: timeout de validación de stock de 30 segundos (consultas de base de datos son rápidas), timeout de pago de 60 segundos (APIs bancarias pueden ser lentas), timeout de asignación de 15 segundos (es un UPDATE simple), y timeout total de saga de 2 horas (permitiendo delays en procesamiento de eventos). Para Inventario: timeout de consulta de base de datos de 5 segundos, timeout de bloqueo pesimista de 10 segundos. Para Pagos: timeout de llamada a pasarela de 30 segundos, 3 reintentos con backoff de 2-8-18 segundos, y timeout de reembolso de 60 segundos.
Reflexiones sobre la Consistencia Eventual
Implementar el patrón Saga es, en esencia, aceptar la naturaleza distribuida de la realidad. No estamos simulando un sistema monolítico con trucos de coordinación; estamos abrazando honestamente que nuestros servicios son entidades autónomas que colaboran a través de eventos. La consistencia eventual no es una limitación que debemos lamentar, sino una propiedad emergente que podemos diseñar deliberadamente.
Los desafíos son reales: flujos implícitos, depuración compleja, compensaciones que requieren pensamiento cuidadoso, race conditions que debemos anticipar, y la necesidad constante de idempotencia. Pero las recompensas también son sustanciales: servicios verdaderamente desacoplados que pueden evolucionar independientemente, escalabilidad horizontal sin límites artificiales, resiliencia ante fallos parciales, y la capacidad de razonar sobre procesos de negocio complejos mediante eventos de dominio.
La clave está en la disciplina. El patrón Transactional Outbox elimina el dual write problem. La deduplicación sistemática maneja mensajes duplicados. Las compensaciones bien diseñadas permiten deshacer operaciones. Los reintentos inteligentes distinguen entre fallos transitorios y permanentes. El versionado cuidadoso permite evolución sin ruptura. La observabilidad exhaustiva ilumina lo que de otra manera sería opaco.
Cada uno de estos elementos es un pilar que sostiene el edificio completo. Eliminar cualquiera de ellos compromete la integridad estructural. Pero implementados en conjunto, con la atención al detalle que merece cada uno, nos permiten construir sistemas distribuidos que no solo funcionan sino que son comprensibles, mantenibles y confiables a largo plazo.
Al final, el patrón Saga nos enseña una lección más amplia sobre la arquitectura de software: los sistemas complejos emergen de la composición de partes simples que interactúan mediante protocolos bien definidos. No necesitamos coordinación central omnisciente. No necesitamos transacciones globales mágicas. Necesitamos servicios que comprendan sus responsabilidades, eventos que comuniquen intenciones claras, y mecanismos de compensación que permitan corrección de errores. Con estos ingredientes, la consistencia eventual emerge naturalmente, como un patrón que se forma en la arena cuando las olas retroceden.
Esta es la belleza del diseño distribuido: aceptamos las limitaciones fundamentales de la física —la información viaja a velocidad finita, los sistemas fallan parcialmente, el tiempo no es absoluto— y construimos abstracciones que funcionan dentro de estas limitaciones en lugar de pretender superarlas. El patrón Saga es nuestra forma de bailar con la entropía en lugar de luchar contra ella.