Event-Driven Architecture
Eventos como fatos
Seção intitulada “Eventos como fatos”Um evento descreve algo que já ocorreu, no passado: PedidoCriado, PagamentoConfirmado, EstoqueReservado. Ele é imutável e não expressa intenção nem comando (“crie o pedido”), apenas registro. Essa distinção é fundamental: comandos têm um destinatário e podem ser rejeitados; eventos são fatos consumados que qualquer interessado pode reagir a.
Isso inverte o controle. Em RPC, o produtor sabe quem chama e espera resposta. Em EDA, o produtor publica e segue adiante; quantos e quais consumidores reagem é problema deles. Adicionar um novo consumidor não exige mudança no produtor.
De Domain Events a eventos de integração
Seção intitulada “De Domain Events a eventos de integração”Dentro de um serviço, Domain Events modelam ocorrências relevantes do domínio (parte do Domain-Driven Design), tipicamente consumidas no mesmo processo. A EDA é a evolução desses eventos para além da fronteira do processo: o domain event que era in-process torna-se um evento de integração publicado no broker para outros serviços. O conceito é o mesmo; muda a escala e o mecanismo de entrega.
Broker: Kafka e RabbitMQ
Seção intitulada “Broker: Kafka e RabbitMQ”O broker é a espinha dorsal. Duas famílias:
- Kafka: log distribuído, particionado e persistente. Eventos ficam retidos e podem ser reprocessados (replay). Consumidores controlam seu offset. Ideal para alto throughput, event sourcing e múltiplos consumidores independentes lendo o mesmo fluxo.
- RabbitMQ: broker de mensagens com exchanges e filas, roteamento flexível e entrega com ack. Forte em enfileiramento de trabalho, roteamento por tópico e cargas com semântica de fila tradicional.
Choreography vs Orchestration
Seção intitulada “Choreography vs Orchestration”Duas formas de coordenar um processo de negócio distribuído:
- Coreografia: cada serviço reage a eventos e publica os seus, sem coordenador central. Máximo desacoplamento, mas o fluxo global fica implícito — difícil de visualizar e depurar.
- Orquestração: um coordenador (orquestrador) comanda os passos explicitamente. Fluxo visível e centralizado, ao custo de reintroduzir um ponto de acoplamento e de conhecimento.
A SAGA pode ser implementada de ambas as formas: a SAGA coreografada é EDA pura (cada passo emite eventos que disparam o próximo e as compensações); a orquestrada usa um coordenador.
sequenceDiagram
participant P as Pedido Service
participant B as Broker
participant Pg as Pagamento Service
participant E as Estoque Service
P->>B: publica PedidoCriado
B-->>Pg: entrega PedidoCriado
Pg->>B: publica PagamentoConfirmado
B-->>E: entrega PagamentoConfirmado
E->>B: publica EstoqueReservado
B-->>P: entrega EstoqueReservado
Note over P,E: Coreografia: sem coordenador central
Consistência eventual e idempotência
Seção intitulada “Consistência eventual e idempotência”EDA abraça a consistência eventual: após um evento, o estado dos consumidores converge com atraso, não instantaneamente. O sistema deve tolerar janelas em que serviços divergem.
Como brokers entregam com semântica at-least-once, o mesmo evento pode chegar duas vezes (redelivery após falha de ack). Consumidores devem ser idempotentes: processar o mesmo evento N vezes produz o mesmo efeito de processá-lo uma vez. A técnica usual é registrar IDs de eventos já processados e ignorar duplicatas.
Publicação confiável: Message Outbox
Seção intitulada “Publicação confiável: Message Outbox”Há uma armadilha clássica: escrever no banco local e publicar no broker não é atômico. Se o processo cai entre as duas operações, ou o estado muda sem evento, ou o evento é publicado sem o estado. O padrão Message Outbox resolve isso: grava-se o evento numa tabela outbox na mesma transação do estado; um processo separado lê a outbox e publica no broker, garantindo que evento e estado sejam consistentes.
Exemplo Java/Spring: produção com outbox e consumo idempotente
Seção intitulada “Exemplo Java/Spring: produção com outbox e consumo idempotente”@Entity@Table(name = "outbox")public class OutboxEvent {
@Id private UUID id;
private UUID aggregateId;
private String tipo;
@Column(columnDefinition = "text") private String payload;
private Instant ocorridoEm;
private boolean publicado;
protected OutboxEvent() { }
public OutboxEvent(UUID aggregateId, String tipo, String payload) { this.id = UUID.randomUUID(); this.aggregateId = aggregateId; this.tipo = tipo; this.payload = payload; this.ocorridoEm = Instant.now(); this.publicado = false; }
public void marcarPublicado() { this.publicado = true; }
public UUID getAggregateId() { return aggregateId; }
public String getPayload() { return payload; }}
@Servicepublic class PedidoAppService {
private final PedidoRepository pedidoRepo; private final OutboxRepository outboxRepo; private final ObjectMapper mapper;
public PedidoAppService(PedidoRepository pedidoRepo, OutboxRepository outboxRepo, ObjectMapper mapper) { this.pedidoRepo = pedidoRepo; this.outboxRepo = outboxRepo; this.mapper = mapper; }
@Transactional public UUID criar(UUID clienteId, BigDecimal total) throws JsonProcessingException { Pedido pedido = pedidoRepo.save(new Pedido(clienteId, total)); String payload = mapper.writeValueAsString( new PedidoCriado(UUID.randomUUID(), pedido.getId(), clienteId, total)); outboxRepo.save(new OutboxEvent(pedido.getId(), "PedidoCriado", payload)); return pedido.getId(); }}
@Componentpublic class OutboxPublisher {
private final OutboxRepository outboxRepo; private final KafkaTemplate<String, String> kafka;
public OutboxPublisher(OutboxRepository outboxRepo, KafkaTemplate<String, String> kafka) { this.outboxRepo = outboxRepo; this.kafka = kafka; }
@Scheduled(fixedDelay = 1000) @Transactional public void publicar() { for (OutboxEvent evento : outboxRepo.findByPublicadoFalseOrderByOcorridoEm()) { kafka.send("pedidos", evento.getAggregateId().toString(), evento.getPayload()).join(); evento.marcarPublicado(); } }}
@Componentpublic class PagamentoConsumer {
private final EventosProcessadosRepository processados; private final PagamentoService pagamentoService; private final ObjectMapper mapper;
public PagamentoConsumer(EventosProcessadosRepository processados, PagamentoService pagamentoService, ObjectMapper mapper) { this.processados = processados; this.pagamentoService = pagamentoService; this.mapper = mapper; }
@KafkaListener(topics = "pedidos", groupId = "pagamento") @Transactional public void onPedidoCriado(ConsumerRecord<String, String> record) throws JsonProcessingException { PedidoCriado evento = mapper.readValue(record.value(), PedidoCriado.class); if (processados.existsById(evento.eventId())) { return; } pagamentoService.cobrar(evento.pedidoId(), evento.total()); processados.save(new EventoProcessado(evento.eventId())); }}
record PedidoCriado(UUID eventId, UUID pedidoId, UUID clienteId, BigDecimal total) {}O consumidor deduplica pelo eventId do payload e registra o ID processado na mesma transação — idempotência mesmo com entrega at-least-once. Repare na divisão de papéis: a chave da mensagem é o id do agregado (preserva a ordem por pedido dentro da partição, como em Message Outbox); o eventId viaja no payload e serve à deduplicação.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Eventos como comandos: nomear no imperativo (
CriarPedido) e assumir um único destinatário reacopla produtor e consumidor. - Consumidor não idempotente: assumir entrega exactly-once; redeliveries geram cobranças/efeitos duplicados.
- Dual write sem outbox: gravar no banco e publicar no broker separadamente, perdendo atomicidade.
- Coreografia para fluxos complexos: sem coordenador, o processo global fica ilegível; considere orquestração.
- Eventos gordos/acoplados: incluir todo o estado interno do produtor no evento acopla o schema dos consumidores ao seu modelo interno.
- Esquecer ordenação: assumir ordem global onde o broker só garante ordem por partição.
Relações
Seção intitulada “Relações”- Raiz conceitual: Observer, Design Patterns (GoF), Domain Events, Domain-Driven Design.
- Coordenação distribuída: SAGA.
- Publicação confiável: Message Outbox.
- Alternativa síncrona: APIs - REST, GraphQL e gRPC.
- Leitura assíncrona de eventos: CQRS.
- Contexto e padrões: Microservices, Microservices Patterns, Sistemas Distribuídos, Home.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”- Qual a diferença semântica entre um evento e um comando, e por que ela importa para o acoplamento?
Resposta
Um evento é um fato passado e imutável (PedidoCriado) sem destinatário definido; um comando expressa intenção futura com um alvo que pode rejeitá-lo. Eventos permitem que N consumidores reajam sem que o produtor os conheça, desacoplando; comandos reintroduzem o conhecimento do destinatário.
- Em que sentido a EDA é “o Observer em escala de sistema”?
Resposta
Ambos desacoplam o sujeito dos observadores: o sujeito não conhece quem reage. No Observer isso ocorre in-process via interfaces; na EDA a lista de inscritos é o broker e a notificação é uma mensagem de rede assíncrona. A EDA também é a evolução dos Domain Events para além do processo.
- Por que consumidores precisam ser idempotentes e como se implementa isso?
Resposta
Porque brokers entregam at-least-once: o mesmo evento pode chegar duas vezes após falhas de ack. Idempotência garante que reprocessar produza o mesmo efeito. Implementa-se registrando IDs de eventos já processados (na mesma transação do efeito) e ignorando duplicatas.
- Qual problema o Message Outbox resolve e como?
Resposta
O dual write: gravar estado no banco e publicar no broker não é atômico, podendo divergir se o processo cair no meio. A outbox grava o evento numa tabela local dentro da mesma transação do estado; um publisher separado lê a tabela e envia ao broker, assegurando que estado e evento sejam consistentes.