Padrões de comunicação assíncrona entre bounded contexts

1. Fundamentos da comunicação entre bounded contexts

Em uma arquitetura baseada em Domain-Driven Design (DDD), um bounded context representa um limite explícito onde um modelo de domínio específico é aplicado. Cada contexto possui sua própria linguagem ubíqua, regras e responsabilidades, o que exige mecanismos de comunicação bem definidos para integração.

A comunicação entre bounded contexts pode ser síncrona ou assíncrona. A comunicação síncrona (como chamadas HTTP diretas) é simples de implementar, mas introduz acoplamento temporal e fragilidade: se um contexto falha, o outro também falha. A comunicação assíncrona, por outro lado, permite desacoplamento, resiliência e escalabilidade, sendo ideal para cenários onde a consistência imediata não é obrigatória.

Os princípios fundamentais incluem:
- Eventos de domínio: notificações sobre mudanças de estado relevantes
- Mensagens: estruturas de dados que transitam entre contextos
- Consistência eventual: aceitar que dados estejam temporariamente inconsistentes

2. Eventos de domínio como espinha dorsal da integração

Eventos de domínio representam fatos ocorridos no domínio que são relevantes para outros contextos. A modelagem adequada inclui:

// Exemplo de evento de domínio (JSON)
{
  "eventId": "e7c3f8a1-9b2d-4e5f-8a6c-1d2e3f4a5b6c",
  "eventType": "PedidoCriado",
  "version": 1,
  "timestamp": "2025-03-20T14:30:00Z",
  "data": {
    "pedidoId": "PED-001",
    "clienteId": "CLI-123",
    "valorTotal": 250.00,
    "itens": [
      {"produtoId": "PROD-456", "quantidade": 2, "precoUnitario": 125.00}
    ]
  }
}

A publicação e assinatura de eventos utilizam message brokers como RabbitMQ ou Kafka:

// Publicação de evento no RabbitMQ (pseudo-código)
channel.basicPublish(
  exchange: "pedidos.exchange",
  routingKey: "pedido.criado",
  body: JSON.stringify(pedidoCriadoEvent),
  properties: {
    deliveryMode: 2, // persistente
    contentType: "application/json"
  }
);

Para garantir idempotência no consumo, cada evento deve conter um identificador único que permita detectar duplicatas:

// Handler com idempotência
function handlePedidoCriado(event) {
  if (processedEvents.has(event.eventId)) {
    return; // já processado
  }
  processedEvents.add(event.eventId);
  // lógica de negócio
}

3. Padrão Event-Driven com filas de mensagens

A arquitetura publish/subscribe permite que um contexto publique eventos sem conhecer os consumidores. As filas de mensagens oferecem recursos como roteamento, tópicos e filas de dead-letter para mensagens que falharam repetidamente:

// Configuração de fila com dead-letter no RabbitMQ
channel.assertQueue("pedidos.fila", {
  durable: true,
  deadLetterExchange: "pedidos.dlx",
  deadLetterRoutingKey: "pedidos.falha",
  messageTtl: 30000 // 30 segundos
});

Para tratamento de falhas, implementa-se políticas de retry com backoff exponencial:

// Retry policy
async function processarMensagem(mensagem) {
  try {
    await processar(mensagem);
    channel.ack(mensagem);
  } catch (error) {
    const retryCount = (mensagem.properties.headers['x-retry-count'] || 0) + 1;
    if (retryCount <= 3) {
      mensagem.properties.headers['x-retry-count'] = retryCount;
      setTimeout(() => channel.nack(mensagem, false, true), retryCount * 5000);
    } else {
      channel.nack(mensagem, false, false); // para dead-letter
    }
  }
}

4. Saga coreografada para transações distribuídas

Uma saga coreografada coordena transações distribuídas através de eventos encadeados entre contextos. Cada contexto executa sua ação local e publica um evento que dispara a próxima etapa:

// Saga de criação de pedido
1. Contexto Pedidos: Cria pedido (status: pendente)
   → Publica: PedidoCriado
2. Contexto Estoque: Reserva itens
   → Publica: EstoqueReservado ou EstoqueInsuficiente
3. Contexto Pagamento: Processa pagamento
   → Publica: PagamentoAprovado ou PagamentoRecusado
4. Contexto Pedidos: Atualiza status do pedido
   → Publica: PedidoConfirmado ou PedidoCancelado

Para ações de compensação:

// Handler de compensação no contexto Estoque
function handlePagamentoRecusado(event) {
  // Libera itens reservados
  liberarEstoque(event.data.pedidoId);
  // Publica evento de compensação
  publishEvent("EstoqueLiberado", { pedidoId: event.data.pedidoId });
}

O monitoramento de sagas é essencial:

// Tabela de monitoramento de saga
sagaId: "SAGA-001"
estado: "EM_ANDAMENTO"
etapaAtual: "RESERVANDO_ESTOQUE"
timeout: "2025-03-20T15:00:00Z"
eventosRecebidos: ["PedidoCriado"]
eventosPendentes: ["EstoqueReservado", "PagamentoAprovado"]

5. CQRS e separação de comandos e consultas

O padrão CQRS (Command Query Responsibility Segregation) separa operações de escrita (comandos) e leitura (consultas). A sincronização entre os modelos de escrita e leitura ocorre via eventos assíncronos:

// Comando (escrita) - processado no contexto de origem
command: CriarPedido
handler: {
  valida dados do pedido,
  salva no banco de escrita,
  publica evento PedidoCriado
}

// Projeção (leitura) - atualizada assincronamente
projeção: PedidosResumo
handler: {
  recebe evento PedidoCriado,
  atualiza materialized view no banco de leitura
}

As materialized views otimizam consultas frequentes:

// Materialized view para consultas de pedidos
CREATE MATERIALIZED VIEW pedidos_resumo AS
SELECT 
  p.id,
  p.cliente_nome,
  p.valor_total,
  p.status,
  COUNT(pi.id) AS quantidade_itens
FROM pedidos p
JOIN pedidos_itens pi ON p.id = pi.pedido_id
GROUP BY p.id;

6. Integração com APIs assíncronas e callbacks

Webhooks permitem que um contexto notifique outro via HTTP, sem polling constante:

// Configuração de webhook
POST /webhooks/registrar
{
  "contexto": "estoque",
  "eventos": ["EstoqueBaixo", "ProdutoIndisponivel"],
  "callbackUrl": "https://pedidos.api/webhooks/estoque"
}

// Notificação via webhook
POST https://pedidos.api/webhooks/estoque
{
  "eventType": "EstoqueBaixo",
  "data": {
    "produtoId": "PROD-456",
    "quantidadeAtual": 5,
    "quantidadeMinima": 10
  }
}

Para request-response assíncrono, utiliza-se correlação de mensagens:

// Envio de requisição assíncrona
const correlationId = uuid.v4();
const replyQueue = await channel.assertQueue('', { exclusive: true });

channel.consume(replyQueue.queue, (msg) => {
  if (msg.properties.correlationId === correlationId) {
    resolve(JSON.parse(msg.content.toString()));
  }
});

channel.sendToQueue('estoque.requests', Buffer.from(JSON.stringify(request)), {
  correlationId: correlationId,
  replyTo: replyQueue.queue
});

7. Tratamento de consistência e conflitos

A consistência eventual é um pilar da comunicação assíncrona. Estratégias para resolução de conflitos incluem:

// Última escrita vence (Last Write Wins)
if (event.timestamp > currentData.lastUpdated) {
  aplicarAtualizacao(event.data);
}

// Versionamento otimista
if (event.version === currentData.version + 1) {
  aplicarAtualizacao(event.data);
} else {
  registrarConflito(event);
  // pode ser resolvido manualmente ou com merge automático
}

A garantia de entrega varia entre exactly-once (ideal, mas difícil) e at-least-once (mais comum, exige idempotência):

// At-least-once com idempotência
function processarEvento(event) {
  if (eventosProcessados.has(event.eventId)) {
    return; // duplicata ignorada
  }
  eventosProcessados.add(event.eventId);
  // lógica de processamento
  eventosProcessados.persist(); // salva estado após processamento
}

8. Monitoramento e governança da comunicação assíncrona

O rastreamento distribuído utiliza correlation IDs para rastrear o fluxo de mensagens entre contextos:

// Correlation ID propagado em todas as mensagens
{
  "correlationId": "CORR-001",
  "spanId": "SPAN-A",
  "parentSpanId": null,
  "eventType": "PedidoCriado",
  "data": { ... }
}

Métricas essenciais para monitoramento:

// Métricas a serem coletadas
- Latência média de processamento por evento
- Throughput (eventos/segundo)
- Tamanho da fila de mensagens pendentes
- Taxa de erro por tipo de evento
- Número de mensagens em dead-letter

O versionamento de contratos permite evolução segura:

// Versionamento de evento
eventType: "PedidoCriado"
version: 2
// Campo antigo removido, novo campo adicionado
data: {
  "pedidoId": "PED-001",
  "cliente": { "id": "CLI-123", "nome": "João" }, // novo formato
  "valorTotal": 250.00,
  "moeda": "BRL" // novo campo
}

Para backward compatibility, consumidores devem aceitar múltiplas versões:

function handlePedidoCriado(event) {
  if (event.version === 1) {
    // compatibilidade com versão antiga
    const clienteId = event.data.clienteId;
    // ...
  } else if (event.version === 2) {
    // novo formato
    const clienteId = event.data.cliente.id;
    // ...
  }
}

Referências