🧠 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á!