Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
    B --> C["Controller / Handler"]
    C --> D["Service Layer (Regras de Negócio)"]
    D --> E["Repository / ORM (Persistência)"]
    E --> F["Banco de Dados / Cache"]

    subgraph ARQ["Arquitetura do Capítulo"]
        G["Conceito: Mensageria Assíncrona e Eventos com RabbitMQ e Kafka"]
        H["Segurança, Validação e Resiliência"]
        I["Alta Performance e Escalabilidade"]
    end

    D --> ARQ

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Fundamentos Técnicos de Mensageria Assíncrona e Eventos com RabbitMQ e Kafka

Por que desacoplar via mensageria. Uma chamada REST síncrona acopla o chamador ao tempo de resposta e à disponibilidade do chamado — se o serviço de faturamento cair, o serviço de pedidos trava junto. Um broker de mensagens rompe esse acoplamento temporal: o produtor publica um evento e segue seu fluxo imediatamente; o consumidor processa quando (e se) estiver disponível, no seu próprio ritmo. RabbitMQ e Kafka resolvem esse problema com filosofias distintas: o RabbitMQ é um broker de filas tradicional (protocolo AMQP), com roteamento flexível via exchanges e filas que descartam a mensagem após consumo confirmado; o Kafka é um log distribuído e particionado, imutável e append-only, onde mensagens permanecem por um período configurável e múltiplos consumidores podem reler o mesmo tópico independentemente — por isso Kafka se encaixa melhor em event streaming de alto volume (analytics, auditoria, CDC), enquanto RabbitMQ se encaixa melhor em filas de trabalho com roteamento complexo (prioridades, TTL por mensagem, dead-lettering nativo).

Produção com RabbitTemplate. rabbitTemplate.convertAndSend(exchange, routingKey, payload) delega a serialização a um MessageConverter — por padrão, Java serializado; em produção, sempre substituído por Jackson2JsonMessageConverter, que grava o payload como JSON legível e interoperável com consumidores não-Java. A exchange decide o roteamento (direct, topic, fanout) e a routing key seleciona qual fila recebe a mensagem — o produtor nunca conhece a fila diretamente, apenas o contrato de roteamento.

Consumo com @RabbitListener e modos de confirmação (ack). Por padrão, o Spring AMQP usa AcknowledgeMode.AUTO: a mensagem é confirmada (removida da fila) automaticamente se o método não lançar exceção, e devolvida à fila (ou rejeitada) se lançar. Em cenários onde a confirmação deve ocorrer só após um efeito colateral crítico (ex.: persistência em banco confirmada), usa-se AcknowledgeMode.MANUAL com Channel.basicAck(deliveryTag, false) explícito — dando controle fino sobre o que conta como “processado com sucesso”. Como o broker garante entrega “at least once” (nunca “exactly once”), a mesma mensagem pode chegar duplicada após uma falha de rede entre o processamento e o ack — por isso, consumidores idempotentes (que verificam uma chave única de mensagem já processada antes de agir) não são opcionais, são um requisito de correção.

Produção e consumo com Kafka. KafkaTemplate<String, T>.send(topico, chave, valor) retorna um CompletableFuture — a chave determina a partição (mensagens com a mesma chave sempre vão para a mesma partição, preservando ordem relativa entre elas, ex.: todos os eventos de um mesmo clienteId). @KafkaListener associa o consumidor a um consumer group: cada partição é consumida por exatamente uma instância do grupo, permitindo paralelismo horizontal simplesmente aumentando o número de instâncias (até o limite do número de partições). O commit do offset (marcador de “até onde já li”) pode ser automático (enable.auto.commit=true, arriscando reprocessar ou perder mensagens dependendo do timing) ou manual via Acknowledgment.acknowledge(), análogo ao ack manual do RabbitMQ.

Dead Letter Queue/Topic e retry com backoff. Uma mensagem “envenenada” (que sempre falha, ex.: JSON malformado) não pode ficar sendo reentregue para sempre — isso bloqueia o processamento de mensagens saudáveis atrás dela. DefaultErrorHandler com FixedBackOff(intervalo, tentativas) reexecuta o listener N vezes com espera entre tentativas e, esgotadas as tentativas, encaminha automaticamente a mensagem para um tópico de Dead Letter (convenção <topico>.DLT no Kafka) ou fila DLQ (no RabbitMQ), isolando o problema para investigação manual sem travar o pipeline principal.

O problema do Dual-Write e o padrão Transactional Outbox. Um serviço que precisa gravar uma entidade no banco e publicar um evento correspondente enfrenta um risco de inconsistência: se o commit no banco tiver sucesso mas a publicação no broker falhar (ou vice-versa), o sistema fica com estados divergentes. O padrão Transactional Outbox resolve isso gravando o evento em uma tabela outbox na mesma transação atômica do banco relacional que grava a entidade de negócio — um processo separado (poller ou Debezium/CDC) lê a tabela outbox e publica os eventos pendentes no broker, garantindo entrega “at least once” sem jamais perder um evento por falha de rede entre banco e broker.


💻 2. Código de Demonstração Corporativo

package com.empresa.api.messaging;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.support.Acknowledgment;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class PedidoConsumer {

    // Cache simples de mensagens já processadas (idempotência). Em produção: Redis/banco.
    private final Set<String> mensagensProcessadas = ConcurrentHashMap.newKeySet();

    @RabbitListener(queues = "pedidos.queue", ackMode = "MANUAL")
    public void receberPedido(PedidoEvento evento, Message message, Acknowledgment ack) {
        String messageId = message.getMessageProperties().getMessageId();

        if (!mensagensProcessadas.add(messageId)) {
            // Mensagem duplicada (redelivery após falha de rede pré-ack): confirma e ignora.
            ack.acknowledge();
            return;
        }

        try {
            processarPedido(evento);
            ack.acknowledge(); // só confirma após o efeito colateral ter sucesso
        } catch (Exception ex) {
            mensagensProcessadas.remove(messageId); // permite reprocessar no próximo retry
            throw ex; // devolve à fila / aciona DLQ conforme política de retry
        }
    }

    private void processarPedido(PedidoEvento evento) {
        System.out.println("Processando pedido idempotente: " + evento.pedidoId());
    }
}
package com.empresa.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.FixedBackOff;

@Configuration
public class KafkaResilienceConfig {

    @Bean
    public DefaultErrorHandler errorHandler() {
        // 3 tentativas com 1s de intervalo; esgotadas, envia para "<topico>.DLT" automaticamente.
        DefaultErrorHandler handler = new DefaultErrorHandler(new FixedBackOff(1000L, 3L));
        handler.addNotRetryableExceptions(IllegalArgumentException.class); // payload inválido: não adianta retry
        return handler;
    }
}