🧠 O problema clássico: você não está mais no seu banco relacional
Lembra daquela época boa em que você dava um @Transactional e tudo era feliz? Commit ou rollback, nunca meio termo. Aí o mundo resolveu ir pra microsserviços, bancos NoSQL, filas, eventos… e essa rigidez ACID foi pro espaço.
O que fazer quando você precisa garantir que várias operações distribuídas aconteçam de forma consistente sem ter um banco que coordene isso tudo?
A resposta vem em quatro letras: BASE (Basic Availability, Soft state, Eventual consistency). E na prática, isso se traduz em:
- Saga Pattern (coreografia ou orquestração)
- Compensação (o tal do rollback manual)
- Idempotência (pra não repetir bagunça)
- Transactional Outbox (pra não perder mensagens)
- Tabela de estado/log (pra saber onde cada saga está)
Vamos destrinchar cada um com exemplos de código (simulando um sistema de e-commerce bem safado).
1️⃣ Saga Pattern – quebre a transação grande em pedaços pequenos
A ideia é linda: em vez de tentar fazer um COMMIT gigante que abrace vários serviços, você divide a operação em pequenas transações locais, cada uma com sua própria ação e sua ação de compensação (caso algo dê errado).
Existem dois jeitos de orquestrar uma saga:
🎭 Coreografia (cada serviço faz sua parte e emite eventos)
Ninguém centraliza o fluxo. Cada serviço escuta eventos e reage.
// Serviço de Pedidos
public void criarPedido(PedidoCommand cmd) {
pedidoRepo.save(cmd); // transação local
eventBus.publish(new PedidoCriadoEvent(cmd.getId(), cmd.getClienteId()));
}
// Serviço de Pagamentos (escuta PedidoCriadoEvent)
@EventListener
public void processarPagamento(PedidoCriadoEvent event) {
try {
pagamentoService.autorizar(event.getClienteId(), event.getValor());
eventBus.publish(new PagamentoAutorizadoEvent(event.getPedidoId()));
} catch (Exception e) {
eventBus.publish(new PagamentoFalhouEvent(event.getPedidoId()));
}
}
// Serviço de Estoque (escuta PagamentoAutorizadoEvent)
@EventListener
public void reservarEstoque(PagamentoAutorizadoEvent event) {
// reserva produtos...
}
Enter fullscreen mode Exit fullscreen mode
Vantagem: baixo acoplamento, cada serviço só conhece seus eventos.
Desvantagem: fluxo fica implícito, difícil de monitorar.
🎼 Orquestração (um serviço central manda em tudo)
Aqui o "maestro" dita cada passo e sabe o que fazer em caso de falha.
public class PedidoOrquestrador {
public void executarSaga(PedidoCommand cmd) {
try {
// Passo 1: Criar pedido
String pedidoId = pedidoClient.criar(cmd);
// Passo 2: Processar pagamento
String pagamentoId = pagamentoClient.autorizar(cmd.getClienteId(), cmd.getValor());
// Passo 3: Reservar estoque
estoqueClient.reservar(cmd.getItens());
// Passo 4: Finalizar
pedidoClient.confirmar(pedidoId);
} catch (PagamentoException e) {
// Compensação do passo 1
pedidoClient.cancelar(pedidoId);
throw new SagaException("Pagamento falhou, pedido cancelado");
} catch (EstoqueException e) {
// Compensação do passo 1 e 2
pedidoClient.cancelar(pedidoId);
pagamentoClient.estornar(pagamentoId);
throw new SagaException("Estoque insuficiente");
}
}
}
Enter fullscreen mode Exit fullscreen mode
Vantagem: fluxo explícito, fácil de logar e monitorar.
Desvantagem: orquestrador vira ponto de acoplamento.
✅ Recomendação: comece com orquestração quando o fluxo for complexo e você precisar de visibilidade. Use coreografia quando cada serviço for realmente independente.
2️⃣ Mecanismo de Compensação – o seu rollback manual
Em sistemas não transacionais, não existe ROLLBACK. Se a etapa 2 falhou, você precisa executar uma ação reversa da etapa 1. É o famoso "desfazer".
Exemplo de compensação em um serviço de reserva de hotéis:
public class ReservaService {
@Transactional
public String criarReserva(ReservaRequest req) {
// ação principal
reservaRepo.save(new Reserva(req.getId(), req.getQuarto(), "CONFIRMADA"));
return req.getId();
}
@Transactional
public void cancelarReserva(String reservaId) {
// ação de compensação
Reserva r = reservaRepo.findById(reservaId);
r.setStatus("CANCELADA");
reservaRepo.save(r);
// libera o quarto, etc.
}
}
Enter fullscreen mode Exit fullscreen mode
Se a saga falhar no meio, o orquestrador chama cancelarReserva para aquela reserva.
Dica de ouro: a ação de compensação também deve ser idempotente (se chamar duas vezes, não faz bagunça). E idealmente, ela nunca falha – mas se falhar, você entra com um processo manual ou fila de dead letter.
3️⃣ Idempotência – pra não repetir a mesma besteira duas vezes
Idempotência é a chave de tudo. Se você não sabe se uma mensagem vai chegar uma ou duas vezes (e no mundo distribuído, ela vai chegar duas vezes), você precisa garantir que o efeito seja o mesmo.
Como implementar: Idempotency Key (também chamada de requestId ou transactionId).
@PostMapping("/pedidos")
public ResponseEntity criarPedido(@RequestBody PedidoReq req,
@RequestHeader("Idempotency-Key") String idempotencyKey) {
// Tabela de operações já processadas
if (processedOperationRepo.existsById(idempotencyKey)) {
Pedido pedido = processedOperationRepo.getResult(idempotencyKey);
return ResponseEntity.ok(pedido); // retorna o mesmo resultado de antes
}
// Processa normalmente
Pedido novoPedido = pedidoService.criar(req);
// Guarda a chave + resultado (na mesma transação!)
processedOperationRepo.save(new ProcessedOperation(idempotencyKey, novoPedido));
return ResponseEntity.ok(novoPedido);
}
Enter fullscreen mode Exit fullscreen mode
Boa prática: a chave de idempotência é gerada pelo cliente (UUID, por exemplo). O servidor só guarda a primeira execução. Qualquer requisição com a mesma chave retorna o resultado em cache.
4️⃣ Transactional Outbox – não perca mensagens (nunca mais)
Quando você salva algo no banco e precisa publicar um evento (tipo "PedidoCriado"), o risco é clássico: você salva no banco, mas antes de publicar o evento o serviço morre. O evento nunca vai pro Kafka, e ninguém sabe que o pedido existe.
A solução é a tabela Outbox (sim, o mesmo padrão do post anterior, mas agora aplicado dentro do serviço):
CREATE TABLE outbox (
id UUID PRIMARY KEY,
aggregate_id VARCHAR(100),
event_type VARCHAR(50),
payload JSONB,
created_at TIMESTAMP,
status VARCHAR(20) DEFAULT 'PENDING'
);
Enter fullscreen mode Exit fullscreen mode
Na mesma transação do seu negócio, você insere o evento na tabela outbox:
@Transactional
public void criarPedido(PedidoReq req) {
// 1. Salva o pedido
pedidoRepo.save(pedido);
// 2. Salva o evento na outbox (MESMA transação)
OutboxEvent event = new OutboxEvent(
UUID.randomUUID(),
pedido.getId(),
"PedidoCriado",
json(pedido)
);
outboxRepo.save(event);
}
Enter fullscreen mode Exit fullscreen mode
Depois, um Message Relay (pode ser um scheduler, Debezium, ou um worker) lê da tabela outbox e publica no Kafka/RabbitMQ, marcando como PUBLISHED.
@Scheduled(fixedDelay = 5000)
public void publicarEventos() {
List<OutboxEvent> pending = outboxRepo.findByStatus("PENDING");
for (OutboxEvent event : pending) {
try {
kafkaTemplate.send(event.getTopic(), event.getPayload());
event.setStatus("PUBLISHED");
outboxRepo.save(event);
} catch (Exception e) {
// log e tenta de novo depois
}
}
}
Enter fullscreen mode Exit fullscreen mode
Garantia: O evento só não será publicado se o serviço morrer antes do commit da transação (mas aí o pedido também não foi salvo). Consistência garantida.
5️⃣ Tabela de Estado ou Log – a memória da saga
Você precisa saber em que ponto cada saga está. Se uma saga falha, quem vai acordar e tentar compensar? Ou se um serviço ficou fora do ar, como retomar?
Crie uma tabela de estado da saga:
CREATE TABLE saga_state (
saga_id UUID PRIMARY KEY,
tipo VARCHAR(50), -- ex: "CRIAR_PEDIDO"
estado VARCHAR(20), -- STARTED, STEP1_OK, STEP2_OK, COMPLETED, FAILED
dados_payload JSONB, -- contexto da saga
created_at TIMESTAMP,
updated_at TIMESTAMP
);
Enter fullscreen mode Exit fullscreen mode
Cada vez que a saga avança, você atualiza o estado (na transação local do passo atual).
Se um passo falha, a saga fica em FAILED. Um processo background (ou o próprio orquestrador) pode reler sagas travadas há muito tempo e executar compensações ou retentativas.
Exemplo de scheduler de monitoramento:
@Scheduled(fixedDelay = 60000)
public void recuperarSagasTravadas() {
List<SagaState> travadas = sagaStateRepo.findByEstadoInAndUpdatedAtLessThan(
List.of("STEP1_OK", "STEP2_OK"),
LocalDateTime.now().minusMinutes(30)
);
for (SagaState saga : travadas) {
log.warn("Saga {} travada no estado {}. Executando compensação...", saga.getId(), saga.getEstado());
orquestrador.compensar(saga);
}
}
Enter fullscreen mode Exit fullscreen mode
🧭 Fluxo de decisão: qual técnica usar quando?
| Seu problema | Técnica principal |
|---|---|
| Coordenar vários serviços em uma operação distribuída | Saga Pattern (orquestração ou coreografia) |
| Preciso desfazer algo que já foi feito | Mecanismo de compensação (ações reversas) |
| O mesmo comando pode chegar duas vezes (e isso é normal) | Idempotência com chave única |
| Preciso publicar eventos sem risco de perder mensagem | Transactional Outbox |
| Saga pode ficar travada e preciso monitorar | Tabela de estado + scheduler de recuperação |
| Tudo isso junto | Combão: Saga orquestrada + idempotência + outbox + tabela de estado |
⚡ Dicas quentes que ninguém conta
1. Compensação não é trivial
Projetar uma ação reversa que sempre funciona é difícil. Por exemplo, se você enviou um e-mail, não dá pra "desenviar". Nesse caso, a compensação pode ser enviar outro e-mail informando o cancelamento.
2. Idempotência tem prazo de validade
Sua tabela de chaves de idempotência vai crescer para sempre. Crie um job que limpa chaves com mais de X dias (ex: 30 dias), a menos que você precise de histórico eterno.
3. Outbox com polling pode ter lag
Se você usa um scheduler simples, seu evento pode demorar alguns segundos para ser publicado. Se precisar de latência baixa, use Debezium (CDC) que reage quase em tempo real.
4. Sagas orquestradas viram ponto único?
O orquestrador pode cair no meio. Nesse caso, você precisa que ele seja resiliente (várias instâncias, persistência de estado, e capacidade de retomar onde parou). A tabela saga_state é fundamental aqui.
5. Coreografia é linda, mas difícil de debugar
Quando tudo é evento, ninguém tem visão completa. Coloque correlation IDs em cada mensagem e use um sistema de tracing (Jaeger, Zipkin) para seguir o fluxo.
💬 Conclusão – ACID morreu, viva a BASE (com planejamento)
Sistemas não transacionais (bancos NoSQL, microsserviços, filas) não te dão rollback mágico. Mas isso não significa que você está condenado à inconsistência. As técnicas que vimos aqui – Saga Pattern, compensação, idempotência, outbox e tabela de estado – são o arsenal moderno para garantir consistência eventual de forma robusta.
Lembre-se:
- Transação grande? Quebre em sagas.
- Falhou no meio? Compense o que foi feito.
- Mensagem repetida? Idempotência resolve.
- Precisa publicar evento? Outbox salva.
- Saga travou? Tabela de estado + scheduler recupera.
Agora é sua vez: já implementou alguma saga na raça? Já perdeu evento porque não usava outbox? Me conta nos comentários. E se ainda não precisou, já sabe onde mirar quando o sistema crescer. Bora codar com menos medo de consistência e mais arquitetura pensada! 🛡️🔥
Quer aprofundar? No próximo post vou mostrar um exemplo prático completo de saga orquestrada com Spring Boot, Kafka e Debezium. Até lá!
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.