Pular para conteúdo

Aula 18 - Arquitetura Orientada a Eventos (EDA) e Mensageria ⚡

Objetivo Pedagógico

Objetivo: Conceber sistemas distribuídos altamente desacoplados e resilientes aplicando Arquitetura Orientada a Eventos (EDA), mensageria assíncrona (RabbitMQ vs Kafka), padrões Event Sourcing e CQRS (Command Query Responsibility Segregation).


📑 1. Fundamentos Teóricos & Análise Técnica

Sistemas construídos exclusivamente sobre chamadas síncronas ponto a ponto (HTTP/REST) criam acoplamento temporal rígido: se um serviço downstream falhar ou sofrer lentidão, toda a cadeia de chamadas cascata em lentidão e falhas generalizadas.

A Arquitetura Orientada a Eventos (Event-Driven Architecture - EDA) resolve esse acoplamento através do paradigma produtor-consumidor: 1. Eventos de Domínio e Assincronismo: - Um evento representa um fato imutável ocorrido no passado do negócio (PedidoCriado, PagamentoConfirmado). - O produtor publica o evento no broker de mensagens sem qualquer conhecimento sobre quem consumirá aquele evento ou quantos consumidores existem. 2. RabbitMQ (Message Broker Tradicional) vs. Apache Kafka (Event Streaming Platform): - RabbitMQ: Focado em roteamento sofisticado de mensagens (Exchange direct, topic, fanout), confirmações individuais (ACK) e descarte da mensagem após o consumo. Ideal para filas transacionais de tarefas e comandos. - Apache Kafka: Log de eventos distribuído, append-only e persistente em disco particionado. Múltiplos consumidores independentes leem o log em suas próprias velocidades através de seus próprios ponteiros (Offsets). Permite reprocessamento histórico (Event Replay). 3. Padrões Avançados: CQRS e Event Sourcing: - CQRS: Separa os modelos de gravação (Comandos que alteram estado) dos modelos de leitura (Consultas altamente otimizadas e desnormalizadas). - Event Sourcing: O estado da aplicação não é gravado como uma linha mutável de banco de dados, mas reconstruído pelo somatório histórico de todos os eventos que já ocorreram na vida do sistema.

📐 Arquitetura Conceitual & Diagrama de Fluxo

graph LR
    Produtor["Serviço de Checkout (Order Service)"] -->|Publica PedidoCriado| Kafka["Broker Kafka: Tópico 'orders.events'"]
    Kafka -->|Consumo Assíncrono Partição 0| Estoque["Serviço de Estoque (Separação)"]
    Kafka -->|Consumo Assíncrono Partição 0| Faturamento["Serviço Fiscal (Nota Fiscal)"]
    Kafka -->|Consumo Assíncrono Partição 0| Notificacao["Serviço de Push / E-mail"]
    Note["Desacoplamento Temporal: Se o Fiscal cair, Checkout continua funcionando normalmente!"]
    style Produtor fill:#e1f5fe,stroke:#01579b
    style Kafka fill:#fff3e0,stroke:#e65100
    style Estoque fill:#f3e5f5,stroke:#7b1fa2
    style Faturamento fill:#ffebee,stroke:#c62828
    style Notificacao fill:#e8f5e9,stroke:#2e7d32

🔍 Pilares e Diretrizes Técnicas

Nesta unidade, aprofundamos os seguintes conceitos fundamentais: - Desacoplamento Temporal e Espacial: Produtores e consumidores operam em momentos e servidores completamente independentes. - Garantias de Entrega (At-least-once / Exactly-once): Configuração rigorosa de confirmações para evitar perda ou duplicação de eventos críticos. - Padrão Outbox Transacional: Gravação atômica da alteração no banco de dados local conjuntamente com a tabela de saída de eventos para evitar inconsistência distribuída. - Escalabilidade Horizontal por Partições: Distribuição paralela de carga do Kafka permitindo milhões de eventos por segundo.


🛠️ 2. Implementação Prática em Arquitetura Orientada a Eventos, Kafka e RabbitMQ

Abaixo está a implementação técnica de referência, estruturada com padrões de engenharia de software e foco em robustez:

// order_event_publisher.ts (Publicação Confiável de Eventos com KafkaJS)
import { Kafka, Partitioners } from 'kafkajs';

const kafka = new Kafka({
  clientId: 'order-service',
  brokers: ['kafka-broker-1:9092', 'kafka-broker-2:9092'],
});

const producer = kafka.producer({
  createPartitioner: Partitioners.DefaultPartitioner,
  idempotent: true, // Garante entrega exatamente-uma-vez sem duplicações
});

interface OrderCreatedEvent {
  orderId: string;
  customerId: string;
  totalAmount: number;
  timestamp: string;
}

export async function publishOrderCreated(event: OrderCreatedEvent) {
  await producer.connect();

  await producer.send({
    topic: 'orders.events',
    messages: [
      {
        key: event.customerId, // Garante que eventos do mesmo cliente caiam na mesma partição!
        value: JSON.stringify(event),
        headers: {
          'event-type': 'OrderCreated',
          'source-app': 'ecommerce-checkout',
        },
      },
    ],
  });

  console.log(`[EVENT PUBLISHED] Evento do Pedido ${event.orderId} emitido com sucesso.`);
}

💡 Análise Passo a Passo do Código

  1. Uso de Chave de Particionamento (key: event.customerId): Garante que todos os eventos do mesmo cliente sejam processados estritamente em ordem na mesma partição do Kafka.
  2. Configuração idempotent: true: Habilita garantias nativas no broker para evitar duplicações causadas por retentativas de rede.
  3. Metadados em Headers: Permite que roteadores e filtros downstream inspecionem o tipo de evento sem deserializar todo o payload.

🎯 3. Próximos Passos & Sequência Didática