Programming On Mars Logo
  • Início
  • Artigos
  • Laboratórios
Programming On Mars Logo
  • Início
  • Artigos
  • Laboratórios

  • Andre Lucas
  • Sun Feb 22 2026

Arquitetura Orientada a Eventos na Prática: Spring Boot, Kafka e PostgreSQL -- e uma IA que Testa Tudo pra Você

Da Teoria pra Trincheira

No artigo anterior, falei sobre Arquitetura Orientada a Eventos como base pra transformação digital. Conceitos como Event Sourcing, CQRS, Saga Pattern -- a teoria por trás de sistemas que se comunicam por eventos em vez de chamadas diretas.

Teoria é importante. Mas teoria sem código é PowerPoint.

Este artigo é o oposto. Aqui, vamos colocar a mão na massa com um microserviço real: Java 25, Spring Boot 4.0, Kafka (via Redpanda), PostgreSQL 16 e Onion Architecture. E o projeto tem um detalhe intencional: ele implementa o Dual Write anti-pattern -- um problema clássico de sistemas distribuídos que a maioria dos desenvolvedores não percebe até quebrar em produção.

Vamos entender o problema, ver o código que o expõe, e discutir como resolvê-lo.


O Que é o Dual Write Problem em Sistemas Event-Driven?

Cenário que a maioria de nós já viveu.

Você salva uma informação no base de dados. Em seguida, publica um evento no Kafka. Duas escritas em dois sistemas separados, dentro da mesma operação.

O que acontece quando o commit no banco funciona, mas o publish no Kafka falha?

sequenceDiagram
    participant HR as HTTP Request
    participant OS as Order Service
    participant DB as Database
    participant K as Kafka

    HR->>OS: POST /orders
    OS->>DB: INSERT order
    DB-->>OS: OK
    OS-xK: publish("order.created") ❌
    Note over OS,K: FALHA — evento nunca entregue

O pedido existe no banco. Nenhum evento foi publicado. Consumidores downstream nunca ficam sabendo. O sistema está inconsistente, e ninguém recebe alerta.

O problema de Dual Write ocorre quando um serviço escreve em dois sistemas separados -- como um banco de dados e um message broker -- sem garantia atômica entre ambos. Se a primeira escrita funciona e a segunda falha, os sistemas ficam silenciosamente inconsistentes.

O @Transactional do Spring cobre o banco. Mas o Kafka vive fora desse boundary transacional. Não existe atomicidade nativa entre os dois.

Construí este projeto pra expor essa falha de forma intencional. Entender o problema é o primeiro passo pra resolvê-lo. Mais adiante neste artigo, mostro como o Transactional Outbox Pattern resolve isso — e como o Mars Enterprise Kit Pro implementa esse padrão end-to-end.


Como o Dual Write Problem Aparece no Spring Boot e Kafka

Agora vamos ao ponto onde a consistência quebra. O OrderService no módulo app/:

// app/ -- @Transactional, mas sem atomicidade com Kafka
@Service
public class OrderService {
    private final CreateOrderUseCase createOrderUseCase;
    private final CancelOrderUseCase cancelOrderUseCase;
    private final OrderRepository orderRepository;

    @Transactional
    public UUID createOrder(Set<OrderItem> items, UUID customerId) {
        return createOrderUseCase.execute(
            new CreateOrderUseCase.Input(items, customerId));
    }
}

E o publisher que implementa a port de eventos:

// app/ -- Kafka Producer (Dual Write)
@Service
public class OrderCreatedPublisher implements OrderEventPublisher {
    private static final String TOPIC = "order.created";
    private final KafkaTemplate<String, String> kafkaTemplate;
    private final ObjectMapper objectMapper;

    @Override
    public void publish(OrderCreatedEvent event) {
        try {
            String payload = objectMapper.writeValueAsString(event);
            kafkaTemplate.send(TOPIC, event.orderId().toString(), payload);
        } catch (JacksonException e) {
            throw new RuntimeException("Failed to serialize OrderCreatedEvent", e);
        }
    }
}

Leia com atenção. O @Transactional envolve o save no banco. O publish no Kafka acontece dentro do mesmo método, mas não faz parte da transação. O risco real:

t=0ms   -> POST /orders chega
t=1ms   -> @Transactional inicia
t=3ms   -> orderRepository.save(order)     [escrita no banco, dentro da transação]
t=5ms   -> orderEventPublisher.publish()   [envio pro Kafka, FORA da transação]
t=6ms   -> Kafka confirma                  [evento está no Kafka]
t=7ms   -> commit do banco falha           [erro de rede, disco cheio, constraint violation]

Resultado: Evento existe no Kafka. Order NÃO existe no PostgreSQL.
           Consumidores downstream processam um pedido fantasma.

Três cenários de falha:

  1. Banco funciona, Kafka falha: pedido salvo, evento nunca publicado. Consumidores downstream nunca sabem que o pedido existe.
  2. Kafka funciona, banco faz rollback: evento publicado, pedido nunca persistido. Consumidores agem sobre um pedido fantasma.
  3. Kafka cai depois do commit: evento perdido silenciosamente.

Sem retry. Sem compensação. Inconsistência silenciosa.

O código parece correto. Compila. Passa nos testes unitários. Funciona em dev. Funciona... até não funcionar.


Prove o Dual Write na Prática: Chaos Testing

Falar sobre falha é uma coisa. Ver a falha acontecer é outra.

O projeto inclui dois cenários de chaos testing que você pode executar pra ver o Dual Write quebrando na sua máquina. Não é simulação -- é a inconsistência real entre PostgreSQL e Kafka.

Cenário 1: Phantom Event (Evento Fantasma no Kafka)

O problema: um evento order.created existe no Kafka, mas o pedido não existe no PostgreSQL. Qualquer consumidor que processar esse evento vai referenciar um pedido fantasma.

O projeto tem um endpoint de chaos (POST /chaos/phantom-event) que usa um interceptor AOP pra forçar um rollback no banco depois que o evento já foi enviado pro Kafka. Pra ativá-lo, inicie a aplicação com o profile chaos:

# Inicie a aplicação com o profile chaos
cd app && SPRING_PROFILES_ACTIVE=chaos mvn spring-boot:run

# Dispare o cenário de phantom event
curl -s -X POST http://localhost:8082/chaos/phantom-event \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "550e8400-e29b-41d4-a716-446655440000",
    "items": [
      {"productId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "quantity": 2, "unitPrice": 149.95}
    ]
  }'

Resposta:

{
  "orderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "existsInDb": false,
  "eventSentToKafka": true,
  "dbRolledBack": true,
  "explanation": "PHANTOM EVENT: The order.created event was published to Kafka, but the order does NOT exist in PostgreSQL. Any consumer processing this event will reference a non-existent order."
}

Verifique você mesmo -- o pedido não existe no banco, mas o evento está no Kafka:

# Pedido NÃO existe no PostgreSQL (rollback)
docker-compose exec postgres psql -U mars -d orders_db -c \
  "SELECT * FROM orders WHERE id = '<orderId>';"
# → (0 rows)

# Evento EXISTE no Kafka
docker-compose exec redpanda rpk topic consume order.created --num 1 --offset end
# → Payload com o orderId fantasma

Como funciona internamente: o PhantomEventChaosAspect é um @Around advice que intercepta o ChaosOrderExecutor.execute(). Ele deixa o use case rodar completamente (INSERT no banco + publish no Kafka), e então lança uma PhantomEventSimulationException. Como a exceção ocorre dentro do boundary @Transactional, o Spring faz rollback no banco -- mas o KafkaTemplate.send() já despachou o evento. Todos os beans de chaos usam @Profile("chaos") e não existem no profile padrão.

Cenário 2: Evento Perdido (Kafka Fora do Ar)

O problema: um pedido é persistido no PostgreSQL, mas o evento order.created nunca é publicado. Consumidores downstream nunca ficam sabendo que o pedido foi criado.

Esse cenário não precisa de endpoint especial. Basta derrubar o Redpanda antes de criar um pedido:

# 1. Crie um pedido baseline (tudo saudável)
curl -s -X POST http://localhost:8082/orders \
  -H "Content-Type: application/json" \
  -d '{"customerId":"550e8400-e29b-41d4-a716-446655440000","items":[{"productId":"6ba7b810-9dad-11d1-80b4-00c04fd430c8","quantity":1,"unitPrice":50.00}]}'
# → 201 Created

# 2. Derrube o Kafka
docker-compose stop redpanda

# 3. Tente criar outro pedido
curl -s -X POST http://localhost:8082/orders \
  -H "Content-Type: application/json" \
  -d '{"customerId":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee","items":[{"productId":"11111111-2222-3333-4444-555555555555","quantity":1,"unitPrice":99.99}]}'

# 4. Suba o Kafka de volta
docker-compose start redpanda
sleep 10

# 5. Compare: banco tem 2 pedidos, Kafka tem só 1 evento
docker-compose exec postgres psql -U mars -d orders_db -c \
  "SELECT COUNT(*) FROM orders;"
# → 2

docker-compose exec redpanda rpk topic consume order.created \
  --format '%v\n' | wc -l
# → 1

O pedido #2 existe no banco mas não tem evento correspondente no Kafka. Nenhum consumidor downstream sabe que ele existe.

Duas Faces do Mesmo Problema

Ambos os cenários são causados pela mesma raiz: sem atomicidade entre PostgreSQL e Kafka.

Cenário 1: Phantom EventCenário 2: Evento Perdido
TriggerAOP força rollback no banco após publishKafka fora do ar durante criação
PostgreSQLPedido NÃO existe (rollback)Pedido EXISTE (commit ok)
KafkaEvento EXISTE (já enviado)Evento NÃO existe (publish falhou)
ImpactoConsumidores processam pedido inexistenteConsumidores nunca sabem do pedido
ReproduçãoPOST /chaos/phantom-event (profile chaos)docker-compose stop redpanda + POST /orders
SoluçãoTransactional Outbox PatternTransactional Outbox Pattern

Ambas as falhas são silenciosas em produção. Sem erros nos logs, sem alertas, sem retries. O sistema continua operando com estado inconsistente entre banco e broker.

Quer reproduzir esses cenários na sua máquina? O repositório está aberto: mars-enterprise-kit-lite. Em 5 minutos você vê o Dual Write quebrando de verdade.


Kafka Consumer: Cancelamento de Pedidos via @KafkaListener

Na outra ponta do pipeline, um consumer escuta eventos de cancelamento:

// app/ -- Consumer Kafka
@Service
public class OrderCancelledConsumer {
    private static final Logger log = LoggerFactory.getLogger(OrderCancelledConsumer.class);
    private final OrderService orderService;
    private final ObjectMapper objectMapper;

    @KafkaListener(topics = "order.cancelled", groupId = "order-service")
    public void onOrderCancelled(String message) {
        try {
            var payload = objectMapper.readValue(message, OrderCancelledPayload.class);
            log.info("Received order.cancelled event for orderId={}", payload.orderId());
            orderService.cancelOrder(payload.orderId());
        } catch (JacksonException e) {
            log.error("Failed to deserialize order.cancelled event", e);
        } catch (BusinessException e) {
            log.warn("Failed to cancel order: {}", e.getMessage());
        }
    }

    record OrderCancelledPayload(UUID eventId, UUID orderId, String reason, Instant occurredAt) {}
}

O sistema usa dois tópicos Kafka:

TópicoPapel
order.createdPublicado pelo app após criar um pedido
order.cancelledConsumido pelo app pra cancelar um pedido

Eventos são JSON puro. Sem Avro, sem Schema Registry. A versão Lite mantém a simplicidade pra focar nos conceitos arquiteturais, não em infraestrutura de serialização.


Subindo o Projeto: Docker Compose

A infraestrutura é toda containerizada:

services:
  postgres:
    image: postgres:16-alpine
    ports: ["5432:5432"]
    environment:
      POSTGRES_DB: orders_db
      POSTGRES_USER: mars
      POSTGRES_PASSWORD: mars

  redpanda:
    image: redpandadata/redpanda:v24.3.1
    ports: ["9092:9092", "8081:8081", "9644:9644"]

  redpanda-console:
    image: redpandadata/console:v2.8.0
    ports: ["8888:8080"]

Redpanda é uma plataforma de streaming compatível com Kafka que roda sem ZooKeeper e sem JVM. Implementa o protocolo Kafka nativamente, funcionando como drop-in replacement pro Apache Kafka em desenvolvimento local e workloads de produção.

Pra rodar:

# 1. Sobe a infraestrutura
docker-compose up -d

# 2. Build de todos os módulos
mvn clean install

# 3. Roda a aplicação
cd app && mvn spring-boot:run

# 4. Cria um pedido
curl -X POST http://localhost:8082/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "550e8400-e29b-41d4-a716-446655440000",
    "items": [
      {"productId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "quantity": 2, "unitPrice": 149.95}
    ]
  }'
ComponenteTecnologiaVersão
LinguagemJava25
FrameworkSpring Boot4.0.3
Build ToolMavenMulti-módulo
Banco de DadosPostgreSQL16 (alpine)
MensageriaRedpanda (compatível com Kafka)v24.3.1
Formato de EventosJSON (Jackson)-
ORMSpring Data JPA-
MigrationsFlyway-
TestesJUnit 5, Mockito, TestContainers, REST Assured-

Clone, suba, teste. Em menos de 5 minutos você tem um microserviço event-driven rodando local. Se curtiu, deixa uma star no repositório — ajuda outros devs a encontrarem o projeto.


AI-First: O Projeto Foi Desenhado pra IA

Aqui a coisa fica interessante.

O projeto foi desenhado desde o início pra ser operado por um agente de IA. Não como um adicional -- como uma restrição de design de primeira classe.

O arquivo CLAUDE.md não é apenas documentação. É um prompt disfarçado de README. Ele diz ao Claude Code:

  • As regras da arquitetura e a direção das dependências
  • O modelo de domínio e suas invariantes
  • As convenções de nomenclatura, teste e empacotamento
  • Como rodar o projeto end-to-end

O diretório .mars/docs/ contém a base de conhecimento da IA -- ADRs, responsabilidades dos módulos, convenções de código.

Comandos customizados e skills do Claude Code:

  • /generate-prp -- gera um Product Requirements Prompt pra uma nova feature, baseado na arquitetura existente
  • /execute-prp -- implementa a feature com TDD (Red, Green, Refactor)
  • chaos-phantom-event -- executa o cenário de Phantom Event end-to-end: sobe a infra, inicia o app com profile chaos, dispara o POST /chaos/phantom-event, e verifica que o evento existe no Kafka mas o pedido não existe no PostgreSQL
  • chaos-testing -- executa o cenário de Evento Perdido: derruba o Redpanda, cria um pedido, e verifica que o pedido existe no banco mas o evento se perdeu no Kafka

A IA não adivinha. Ela lê as regras, entende os boundaries, e opera dentro deles. Os chaos skills vão além -- eles provam os problemas arquiteturais que o artigo descreve, automatizando a reprodução das falhas de Dual Write. Isso é Context Engineering aplicado à infraestrutura de desenvolvimento.


Smoke Test End-to-End com IA e Docker Compose

Quando o Claude Code roda o smoke test, essa é a sequência:

1. docker compose up -d          (PostgreSQL 16 + Redpanda)
2. Espera os serviços ficarem healthy
3. Verifica se os tópicos Kafka existem  (order.created + order.cancelled)
4. POST /orders                  -> valida 201 Created + orderId
5. Consome evento order.created  -> valida que orderId confere
6. Publica order.cancelled       -> { orderId, reason: "smoke-test" }
7. GET /orders/{orderId}         -> valida status = CANCELLED
8. docker compose down
9. Reporta PASS ou FAIL com logs

Ciclo completo validado: cria via REST, publica no Kafka, consome cancelamento, atualiza estado, verifica via REST.

Além do smoke test, o Claude Code executa os chaos skills automaticamente:

# Phantom Event -- prova que Kafka tem evento pra pedido que não existe no banco
Run the chaos-phantom-event skill

# Evento Perdido -- prova que banco tem pedido sem evento no Kafka
Run the chaos-testing skill with scenario lost-event

O que um dev junior levaria 30 minutos pra fazer manualmente, a IA faz em segundos -- e repete exatamente da mesma forma toda vez.


Por Que Design AI-First Beneficia Todo Desenvolvedor

O smoke test não é o ponto. O design é o ponto.

Quando você estrutura um projeto pra que uma IA consiga operá-lo end-to-end, você é forçado a tornar tudo explícito. As regras de arquitetura vivem num documento, não na cabeça de alguém. As convenções estão escritas, não são conhecimento tribal. A sequência de teste é um script, não um checklist manual.

Isso beneficia todo desenvolvedor do time, não apenas a IA.

O CLAUDE.md funciona como documentação de onboarding. O .mars/docs/ é a base de conhecimento da arquitetura. A sequência do smoke test são os critérios de aceite pra "o sistema funciona".

Design AI-First não é sobre substituir desenvolvedores. É sobre fazer o projeto tão bem documentado e tão explícito que qualquer um -- humano ou IA -- consiga operá-lo com confiança.


Boas Práticas

Faça:

  • Isole o domínio de qualquer framework -- ele deve ser Java puro
  • Defina ports como interfaces no módulo de negócio, adapters nas camadas externas
  • Documente a arquitetura num CLAUDE.md ou equivalente -- humanos e IAs se beneficiam
  • Exponha anti-patterns intencionalmente em projetos educacionais -- ensina mais que acertar de primeira. Melhor ainda: construa chaos tests que reproduzam as falhas -- um endpoint de phantom event ou um script que derruba o Kafka torna o problema tangível pro time inteiro
  • Use Docker Compose pra tornar o projeto reproduzível em qualquer máquina

Não faça:

  • Nunca assuma que @Transactional cobre operações fora do banco
  • Nunca publique eventos sem considerar o que acontece se o broker estiver fora
  • Nunca coloque dependências de framework no módulo de domínio
  • Nunca vá pra produção com Dual Write sem Transactional Outbox ou equivalente

Resolvendo o Dual Write: Transactional Outbox Pattern

Você acabou de ver o problema quebrando. Phantom events, eventos perdidos, inconsistência silenciosa. Em dev, isso é um exercício. Em produção, isso é uma sexta-feira à noite com o celular tocando.

O problema de Dual Write neste projeto é intencional. Deixei ele lá pra você ver, entender, e sentir por que importa. Em um sistema de produção, você nunca colocaria isso no ar sem uma solução.

O Transactional Outbox Pattern resolve o problema de Dual Write escrevendo o evento numa tabela outbox dentro da mesma transação de banco que os dados de negócio. Um processo separado faz polling da outbox e publica os eventos no message broker. Como a escrita de negócio e a escrita do evento compartilham uma única transação, a atomicidade é garantida.

O Mars Enterprise Kit Pro resolve com esse padrão — e vai muito além. Ele inclui Helm charts, pipelines CI/CD, Apache Avro com Schema Registry, observabilidade com OpenTelemetry, e padrões como SAGA e CQRS. É a evolução production-grade do que a versão Lite ensina.

Se a versão Lite já tem chaos testing com AOP e reprodução automatizada de falhas, imagina o que a versão Pro entrega.

Você precisa entender o problema antes de valorizar a solução. É por isso que o Lite vem primeiro.

FeatureLite (Gratuito)Enterprise Kit Pro
Onion ArchitectureSimSim
Kafka + PostgreSQLSimSim
Design AI-FirstBásicoCompleto
Transactional Outbox PatternNãoSim
Apache Avro + Schema RegistryNãoSim
Helm charts / KubernetesNãoSim
Pipelines CI/CDNãoSim
Observabilidade OpenTelemetryNãoSim
Padrões SAGA / CQRSNãoSim
Production-ReadyNãoSim

O Lite te ensina o problema. O Pro te dá a solução. Veja o que muda →


Conclusão: Entendendo o Dual Write Antes de Resolver

Este projeto existe pra fechar a lacuna entre teoria e prática em Arquitetura Orientada a Eventos. O artigo anterior cobriu os conceitos. Este mostrou o código.

O Dual Write é o ponto central. Não porque é a resposta certa, mas porque expor o problema é a melhor forma de ensinar por que a solução importa. E agora você pode provar o problema na sua máquina: dispare o POST /chaos/phantom-event e veja o evento fantasma no Kafka, ou derrube o Redpanda e veja o evento perdido. Não é teoria -- é inconsistência real que você pode observar, debugar e entender.


Quer experimentar? Clone e quebre coisas.

O Mars Enterprise Kit Lite é gratuito, open-source e roda na sua máquina em 5 minutos. Clone, suba o Docker Compose, rode os chaos tests e veja o Dual Write quebrando de verdade.

github.com/andrelucasti/mars-enterprise-kit-lite

Leia o CLAUDE.md e deixe o Claude Code reproduzir as falhas de Dual Write pra você.

Se o projeto te ajudou a entender o Dual Write, deixa uma star no repo. Custa zero e ajuda outros devs a encontrarem esse conteúdo.


Precisa disso em produção?

O Mars Enterprise Kit Pro resolve o Dual Write com Transactional Outbox Pattern implementado end-to-end — e inclui tudo que falta pra ir pra produção: Helm charts, CI/CD, Apache Avro com Schema Registry, observabilidade com OpenTelemetry, SAGA e CQRS.

Conheça o Mars Enterprise Kit Pro →


Se quiser trocar ideias sobre arquitetura event-driven, Onion Architecture ou design AI-First, me procure no LinkedIn.


Referências

  • Mars Enterprise Kit Lite - GitHub
  • Mars Enterprise Kit Pro
  • Programming on Mars
  • Apache Kafka Documentation
  • Redpanda Documentation
  • Spring Boot 4.0 Reference
  • Transactional Outbox Pattern - Microservices.io
  • Onion Architecture - Jeffrey Palermo
  • The Practical Onion Architecture — André Lucas
Tags:
JavaSpring BootKafkaPostgreSQLEvent-Driven ArchitectureOnion ArchitectureRedpandaDual WriteMicroservicesAI-First
  • Política de Privacidade
  • Termos de Serviço
  • Contato
© 2025 Programming On Mars. Todos os direitos reservados.