Saltar a contenido

ADR-0004 · Eventing: EventBridge dominio + SNS outbound + SQS bulkhead + Outbox

  • Status: Accepted
  • Date: 2026-04-30
  • Deciders: Cristian Fernández (Zerviz Group)
  • Related: ADR-0003 (DDB Streams), ADR-0007 (Strategy), docs/discovery/00-phase1-prerequisites.md 7.1/7.2/7.3.

Context

El legacy es síncrono y sin DLQ; cualquier latencia en Five9/Meta paga el cliente final (R-07). La decisión asentada es híbrida: EventBridge para eventos de dominio, SNS para notificaciones outbound (alineado con el Lab golden path), SQS+DLQ por contexto, circuit breaker en SSM.

Decision

EventBridge (eventos de dominio)

  • Bus dedicado por env: ze-{env}-domain-events (no usar default).
  • Naming de eventos: Source = ze.<service>, DetailType = <Aggregate>.<Event> en PascalCase. Ej: Source=ze.inbound-router, DetailType=Conversation.MessageReceived.
  • Detail payload obligatorio:
    {
      "tenant_id": "uuid",
      "correlation_id": "uuid",
      "occurred_at": "ISO8601",
      "schema_version": "1.0",
      "data": { ... }
    }
    
  • Schemas: EventBridge Schema Registry con discovery activo en dev; promotion manual a qa/prod.
  • Reglas declarativas: un consumer = una rule + una SQS objetivo (Pipes para fan-out preservando orden cuando aplique). Filtros por detail.tenant_id para tier físico (multi-cuenta cross-account event publishing).
  • Cross-account: las cuentas tenant físico tienen permission policy que permite a ze-prod-domain-events recibir/emitir eventos cruzados.

SNS (notificaciones outbound)

  • Topic: ze-{env}-outbound-notifications para fan-out simple (cuando un evento debe ir a múltiples destinos sin lógica de filtrado, ej. webhooks de cliente final, métricas externas).
  • No se usa SNS para eventos de dominio (esos van por EventBridge).

SQS bulkhead + DLQ (asentado 7.2)

  • Patrón obligatorio: cada consumer EventBridge tiene su SQS principal + DLQ. Nunca un consumer lee directo de EventBridge (siempre vía SQS).
  • Naming: ze-{env}-<service>-<event-name> y ze-{env}-<service>-<event-name>-dlq.
  • Retry policy: maxReceiveCount = 5. Tras 5 fallos pasa a DLQ.
  • Visibility timeout: 6× timeout del Lambda consumer.
  • Bulkhead inbound vs campañas: colas separadas para inbound-router (alta prioridad, baja latencia) y outbound-dispatcher (best-effort, throughput).
  • DLQ alarms: CloudWatch alarm ApproximateNumberOfMessagesVisible > 0 durante 5 min ⇒ PagerDuty.

Outbox Pattern

  • DDB Streams habilitado en tablas que requieren atomicidad DB ↔ evento: ze-flow-state, ze-outbound-runs, ze-tenants y ze-conversations (cuando exista).
  • Lambda EventPublisher consume el stream y publica a EventBridge con idempotencia (key = <tableName>#<eventID>).
  • Esto garantiza "una y sólo una" publicación incluso ante crash del servicio que escribió en DDB.

Circuit Breaker (asentado 7.3)

  • Estado persistido en SSM Parameter Store: /ze/{env}/breakers/<connector>/<state> con valores closed | open | half-open, last_failure_at, failure_count.
  • Middleware en packages/connector-sdk/breaker.ts: aplica patrón Hystrix-like, con threshold por defecto 5 fallos en 60 s ⇒ open por 30 s; un trial request ⇒ half-open.
  • Métricas: custom BreakerOpenCount, BreakerHalfOpenTrials vía Powertools metrics.

Consequences

Positive

  • Inbound robusto ante caídas de Five9/Meta/360 (mensaje espera en SQS hasta procesar).
  • Outbox elimina ventanas de inconsistencia DB ↔ evento.
  • Cross-account events permiten que tier físico participe del event mesh.

Negative

  • 4 piezas (EventBridge, SNS, SQS, SSM) ⇒ curva de aprendizaje. Mitigado con infra/modules/eventing/ que provisiona el patrón completo en una llamada.
  • Outbox vía Streams agrega latencia ~1 s entre commit DB y publicación.

Alternatives considered

  • Sólo SNS+SQS (Lab golden path): rechazado, EventBridge da Schema Registry + filtros declarativos + Pipes nativos.
  • MSK / Kafka: descartado (overkill, costo, ops).
  • EventBridge sin SQS bulkhead: rechazado, los retries de EventBridge son limitados; SQS da control fino.
  • Step Functions Standard para todos los flows: descartado por costo; sí se usa Express dentro de outbound-dispatcher Saga.

Tenant-isolation impact

  • tenant_id obligatorio en detail de cada evento. Rules pueden filtrar por tenant.
  • Cross-account: permission policy granular por cuenta tenant físico.

Blast radius

  • DLQ por consumer = fallos contenidos. Un connector caído no detiene a los demás.
  • Saga (Step Functions) compensación garantiza rollback de campañas parciales.

Cost note

  • EventBridge: USD 1.00 / millón de eventos.
  • SQS: USD 0.40 / millón de requests.
  • SNS: USD 0.50 / millón de publishes.
  • Dev: < USD 5/mes total. Prod (10k msg/día): ~USD 30/mes.

ISO 27001 controls touched

  • A.12.4.1 (event logging): EventBridge histórico + DLQ traces.
  • A.13.2.1 (políticas de transferencia de información): cross-account permission policy.
  • A.14.2.5 (secure engineering): outbox pattern garantiza consistencia.

Sources

  • docs/discovery/00-phase1-prerequisites.md 7.1, 7.2, 7.3.
  • docs/discovery/07-lab-golden-path.md (patrón SNS+SQS adaptado).
  • AWS docs: EventBridge Schema Registry, Pipes, SSM Parameter Store.