Pular para o conteúdo

Event-Driven Architecture

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.

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.

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.

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

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.

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;
}
}
@Service
public 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();
}
}
@Component
public 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();
}
}
}
@Component
public 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.

  • 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.
  1. 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.

  1. 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.

  1. 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.

  1. 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.