Executando verificação de segurança...
7

O Desafio das Transações em Arquiteturas Distribuídas (SAGA)

A ideia deste post nasceu de um problema real que enfrentei, estudei como implementar, como tive exito na tarefa, resolvi escrever o post para fixar mais o aprendizado justamente para ter um material guia para o meu 'EU' do futuro geralmente eu faço isso no particular em meus cadernos e anotações, mas hoje resolvi deixar publico, algumas partes do texto as seguir foi formatada com IA, boa leitura e bom divertimento:

Imagine que você está desenvolvendo o backend de um e-commerce. Em uma arquitetura de microsserviços, o processo de finalização de uma compra não é uma única operação, mas uma série de etapas coordenadas entre serviços independentes. Por exemplo, um pedido pode envolver:

  1. Serviço de Pedidos: Cria um novo pedido com o status "pendente".
  2. Serviço de Pagamentos: Processa o pagamento do cliente.
  3. Serviço de Inventário: Deduz os itens comprados do estoque.

Em um mundo ideal, todas essas etapas ocorrem sem falhas. Mas o que acontece se o serviço de pagamentos aprova a transação, mas o serviço de inventário descobre que o último item do estoque estava, na verdade, danificado e não pode ser enviado?

Nesse ponto, temos um problema sério de inconsistência de dados. O cliente foi cobrado por um produto que não receberá. Precisamos de uma forma de "desfazer" o pagamento que já foi processado. Em um sistema monolítico com um único banco de dados, poderíamos simplesmente usar uma transação ACID (Atomicidade, Consistência, Isolamento, Durabilidade) e reverter tudo com um ROLLBACK. No entanto, em um ambiente de microsserviços, cada serviço gerencia seu próprio banco de dados. Não existe um ROLLBACK mágico que abranja múltiplos bancos de dados e serviços.

Essa é a essência do problema: como garantir a consistência dos dados em operações que se estendem por múltiplos serviços independentes?

Pense nisso como planejar uma viagem de férias que envolve três reservas separadas: um voo, um hotel e um carro alugado. Você reserva o voo com sucesso. Em seguida, tenta reservar o hotel, mas descobre que não há quartos disponíveis para as datas desejadas. Você não pode simplesmente continuar com a viagem. É preciso cancelar o voo que já foi reservado para evitar ser cobrado por algo que não usará. Cada reserva é uma transação local, e o cancelamento do voo é a "ação compensatória" necessária para reverter o processo.

A falha em gerenciar essa sequência de operações e suas possíveis reversões pode levar a estados de dados inconsistentes, resultando em prejuízos financeiros, problemas de estoque e, o mais importante, clientes insatisfeitos. É exatamente para resolver esse complexo quebra-cabeça de transações distribuídas que o padrão SAGA foi criado.

Este código não implementa o padrão SAGA intencionalmente. O objetivo aqui é demonstrar claramente o problema da inconsistência em uma arquitetura distribuída, preparando o terreno para a solução SAGA que virá a seguir.

sem_orquestrador

Como Testar o Problema

Depois de executar o código com uvicorn main:app --reload, você pode usar uma ferramenta como o Postman, Insomnia ou curl para testar os cenários:

  1. Cenário de Sucesso:
POST http://127.0.0.1:8000/orders
Body (JSON): {"item_id": "item_123", "quantity": 1, "customer_id": "cliente_feliz"}
Resultado: O pedido será concluído, o pagamento aprovado e o estoque deduzido. O sistema permanece consistente.
  1. Cenário de Falha no Pagamento:
POST http://127.0.0.1:8000/orders
Body (JSON): {"item_id": "item_123", "quantity": 1, "customer_id": "cliente_sem_saldo"}
Resultado: O pagamento será recusado. O pedido será marcado como FALHA_PAGAMENTO. Este cenário é "seguro", pois nenhuma transação financeira ou de estoque foi efetivada.
  1. Cenário de Inconsistência (O Problema Real):
POST http://127.0.0.1:8000/orders
Body (JSON): {"item_id": "item_123", "quantity": 10, "customer_id": "cliente_com_saldo"} (quantidade maior que o estoque)
Resultado:
O serviço de Pedidos cria o pedido como PENDENTE.
O serviço de Pagamentos aprova o pagamento (pois o customer_id não contém "sem_saldo").
O serviço de Inventário falha, pois a quantidade (10) é maior que o estoque (5).
O serviço de Pedidos captura o erro e retorna uma mensagem de INCONSISTÊNCIA DE DADOS.
O estado final é inconsistente: o pagamento foi registrado em db_payments, mas o pedido está marcado como falho em db_orders e o estoque não foi alterado. O cliente foi cobrado por nada.

Este exemplo prático expõe a fragilidade de uma coreografia simples de chamadas diretas entre serviços. A falha em uma etapa tardia do processo, após outras etapas já terem confirmado suas transações locais, deixa o sistema em um estado corrompido. É precisamente para gerenciar essa complexidade, garantindo que seja possível reverter transações anteriores, que introduziremos o orquestrador SAGA.

Com o orquestrador:

O orquestrador é o componente central que conhece todas as etapas do processo. Ele chama cada serviço, aguarda a resposta e, se algo falhar, é sua responsabilidade invocar as ações compensatórias para reverter as operações que já foram concluídas com sucesso.

O Código com o Padrão SAGA (Orquestração)

As principais mudanças são:

Novos Endpoints de Compensação: Criamos endpoints como /payments/refund e /inventory/restore que desfazem as ações originais.
Lógica no Orquestrador: O endpoint /orders/saga agora contém a lógica para executar a sequência de etapas e, crucialmente, a lógica para executar as compensações em ordem inversa em caso de falha.

com_orquestrador

Como Testar a Solução SAGA

  1. Cenário de Sucesso:
POST http://127.0.0.1:8000/orders/saga
Body (JSON): {"item_id": "item_123", "quantity": 2, "customer_id": "cliente_com_saldo"}
Resultado no Console:
🚀 SAGA INICIADA para o pedido order_...
  -> Executando etapa: Pagamento...
✅ PAGAMENTO: Pagamento para o pedido order_... APROVADO.
  -> Executando etapa: Inventário...
✅ INVENTÁRIO: Estoque do item item_123 deduzido para 3.
✅ SAGA CONCLUÍDA com sucesso para o pedido order_...!

Estado Final: O sistema está consistente. O pagamento foi aprovado e o estoque deduzido.
  1. Cenário de Falha e Rollback (O Teste Real):
POST http://127.0.0.1:8000/orders/saga
Body (JSON): {"item_id": "item_123", "quantity": 10, "customer_id": "cliente_com_saldo"} (quantidade maior que o estoque)
Resultado no Console:
🚀 SAGA INICIADA para o pedido order_...
  -> Executando etapa: Pagamento...
✅ PAGAMENTO: Pagamento para o pedido order_... APROVADO.
  -> Executando etapa: Inventário...
❌ FALHA na etapa! Detalhe: {"detail":"Estoque insuficiente ou item não encontrado."}
⏪ INICIANDO ROLLBACK para o pedido order_...
  -> Revertendo etapa via /payments/refund...
⏪ PAGAMENTO: Pagamento para o pedido order_... ESTORNADO.

Estado Final: O sistema permanece consistente. Embora a transação tenha falhado, a ação de compensação (estorno do pagamento) foi executada, garantindo que o cliente não fosse cobrado indevidamente. O estado final é como se a transação nunca tivesse ocorrido.

Este exemplo demonstra o poder do padrão SAGA de orquestração. Ele introduz uma complexidade maior no serviço coordenador, mas em troca oferece uma garantia robusta de consistência de dados em toda a arquitetura, tratando falhas de forma elegante e previsível.

Em suma, o padrão SAGA, especialmente no modelo de orquestração, emerge como uma solução elegante e pragmática para o desafio da consistência de dados em arquiteturas de microsserviços. Ao invés de depender de transações distribuídas frágeis, ele abraça a eventualidade de falhas, garantindo que para cada passo bem-sucedido, exista uma ação compensatória correspondente capaz de reverter o processo e manter o sistema íntegro. A implementação de um orquestrador, como demonstramos com FastAPI, centraliza a lógica da transação de negócio, adicionando uma camada de complexidade que é amplamente compensada pela resiliência, previsibilidade e pela confiança que traz ao sistema. Adotar o padrão SAGA não é apenas sobre programar para o "caminho feliz", mas sim projetar sistemas que se comportam de maneira segura e consistente diante do inevitável: a falha. Para qualquer desenvolvedor que projeta sistemas distribuídos complexos, dominar essa técnica é um passo fundamental na construção de aplicações verdadeiramente robustas e confiáveis.

Carregando publicação patrocinada...
1

eu adicionaria uma etapa para reservar o item em estoque e só entao fazer o pagamento, isso diminuiria a ocorrência de estornos. Então ficaria:

  1. Reserva o estoque
  2. Efetua Pagamento
  3. Deduz estoque

2.1 No caso de pagamento recusado da rollback na reserva (mais comum)

3.1 No caso de erro ao deduzir estoque, so ai rollback no pagamento com refund (seria um edge case)

Ou ainda, considerar se implementar apenas consistência eventual com outbox pattern nao atende o use case. Se o negócio não tolera janela de inconsistência (precisa “tudo ou nada” imediato) ai não tem jeito, tem que ir de saga mesmo.

1

Algo que achei curioso foi a decisão de separar cada serviço em bancos diferentes, mesmo todos parecendo ser relacionais. Ao meu ver, essa separação adiciona uma complexidade que pode não ser necessária, principalmente em projetos que ainda não lidam com alta escala.

Com um único banco, você consegue manter rollback transacionais de forma muito mais simples, sem precisar de um mecanismo compensatório como o SAGA logo de início. Além disso, se em algum momento o sistema realmente exigir mais performance, é sempre possível evoluir: usar réplicas de leitura, particionamento ou até migração para diferentes tecnologias de banco por domínio específico. Ou seja, você não perde a capacidade de escalar futuramente, mas ganha simplicidade no começo do projeto e uma manutenção bem mais tranquila.

Entendo que um dos principais argumentos para separar bancos é o alinhamento com a filosofia de microsserviços: cada serviço teria total autonomia sobre seu domínio e dados, evitando acoplamento. Também faz sentido em termos de bounded contexts (DDD), já que cada equipe pode focar no seu pedaço sem se preocupar com o schema de outro serviço.

Mas, na prática, muitos sistemas começam menores e não precisam dessa independência toda. O custo de manter múltiplos bancos, gerenciar consistência distribuída, lidar com falhas e implementar mecanismos compensatórios (como SAGA) pode ser alto demais comparado ao benefício imediato. Pessoalmente, nesse trade-off, eu priorizaria a simplicidade: começar com um banco único, aceitar um acoplamento inicial (administrável) e só separar quando houvesse uma necessidade real, seja por performance, compliance ou evolução tecnológica.

Em resumo, faz sentido separar bancos quando o contexto do projeto realmente exige isolamento, auditoria e evolução independente de cada domínio. Mas se o cenário não demanda isso, a decisão pode acabar antecipando complexidade sem ganho real. Vale repensar junto ao time (se tiver) e se essa é uma necessidade de agora ou algo que pode (e deve) ser adiado para o momento certo.

1

Então, não foi uma decisão minha, exites atualmente dois sistemas rodando que fizemos a integração, o terceiro banco é do próprio sistema que estamos construindo.
Foi uma imposição das circunstâncias. Além de que não foi minha primeira opção para resolver o problema foi minha terceira kkk.