20 de janeiro de 2026
Saga Pattern: garantindo consistência em arquiteturas de microsserviços
Então você finalmente migrou seu monolito para microsserviços. Parabéns! Agora cada serviço tem seu próprio banco de dados, tudo está desacoplado e você acha que está tudo sob controle — até chegar aquele requisito aparentemente simples:
"precisamos processar um pedido que envolve pagamento, estoque E notificação ao cliente"
Pronto. Bem-vindo ao inferno das transações distribuídas.
Lembra quando tudo era simples no monolito? Você abria uma transação ACID, executava suas operações e, se algo desse errado, um ROLLBACK resolvia tudo.
Mas agora você tem:
- Um serviço de pedidos e estoque com seu próprio banco relacional
- Um serviço de pagamento com MongoDB
- Um serviço de notificações
E todos precisam trabalhar de forma coordenada. Se o pagamento falha depois de você já ter baixado o estoque, o que fazer? Se a notificação não sai, é preciso reverter tudo?
Usar BEGIN TRANSACTION não vai funcionar aqui.
É aí que entra o Saga Pattern, que é basicamente uma solução para gerenciar transações que envolvem múltiplos serviços. Como Chris Richardson (microservices.io) menciona em seu artigo:
"Uma saga é uma sequência de transações locais. Cada transação local atualiza o banco de dados e publica uma mensagem ou evento que dispara a próxima transação local da saga. Se uma transação local falha porque viola uma regra de negócio, a saga executa uma série de transações compensatórias que desfazem as mudanças feitas pelas transações anteriores."
A ideia é quebrar aquela "grande transação" em várias transações menores, cada uma no seu serviço. E aqui está a parte interessante:
se algo dá errado, você não faz rollback — você executa transações compensatórias.
O que são transações compensatórias? É como o "Ctrl+Z" dos microsserviços. Uma transação compensatória é basicamente o oposto da transação original. Reservou um voo? A compensação é cancelar a reserva. Cobrou o cartão? A compensação é estornar, e assim por diante.
Pense naquele exemplo clássico de viagem (que todo artigo sobre Saga usa, mas é tão bom que vou usar também 😅). Imagine uma aplicação em que:
- Você reserva um voo
- Depois reserva um quarto de hotel
- E, por fim, aluga um carro
Se o último passo, o aluguel do carro, falha, a aplicação precisa:
Primeiro: cancelar a reserva do hotel
Segundo: cancelar a reserva do voo
Essas são suas transações compensatórias. Simples, não?
As duas faces da Saga: orquestração vs coreografia
Existem duas formas principais de implementar o Saga Pattern, e cada uma tem seus trade-offs.
1. Orquestração
Na orquestração, você tem um orquestrador central que coordena tudo. Ele conhece a ordem das operações, quando chamar cada serviço e quando executar as compensações.
Cliente → Orquestrador → Serviço A → Orquestrador → Serviço B → Orquestrador → Serviço C ...
Vantagens:
- Mais fácil de depurar (você sabe exatamente onde está)
- Lógica centralizada
- Mais fácil de visualizar o fluxo
Desvantagens:
- Ponto único de falha (se o orquestrador cai, acabou)
- Pode virar um "God Object" rapidinho
- Acoplamento com o orquestrador
A AWS tem um ótimo exemplo usando Step Functions na documentação, onde cada passo tem seus próprios handlers de sucesso e falha. Exemplo da AWS
2. Coreografia
Na coreografia, cada serviço sabe o que fazer quando recebe um evento e publica novos eventos para o próximo da cadeia.
Serviço A → Evento: "A_COMPLETED" → Serviço B → Evento: "B_COMPLETED" → Serviço C
Vantagens:
- Totalmente desacoplado
- Sem ponto único de falha
- Mais "microservice-like"
Desvantagens:
- Difícil de depurar (a lógica fica espalhada)
- Pode virar um "inferno de eventos" rapidamente
- Entender o fluxo completo exige olhar N serviços
Como o pessoal da Baeldung menciona, coreografia é melhor para fluxos simples e orquestração para os mais complexos.
Implementando na prática
Vou mostrar um exemplo conceitual em PHP usando RabbitMQ com orquestração (porque é mais fácil de entender).
Vou assumir que você já configurou o projeto com as bibliotecas necessárias, então foco só no que importa.
Criamos então a classe TravelBookingSagaOrchestrator, que vai orquestrar a Saga, e nela criamos as seguintes funções:
public function bookTrip(array $tripData): array
{
$sagaId = uniqid('trip_', true);
$this->sagaLog = [];
try {
$flight = $this->executeStep('flight', 'reserve', [
'from' => $tripData['from'],
'to' => $tripData['to'],
'date' => $tripData['departure_date'],
'passengers' => $tripData['passengers']
], $sagaId);
$this->logStep('flight_reserved', $flight);
$hotel = $this->executeStep('hotel', 'reserve', [
'city' => $tripData['to'],
'checkin' => $tripData['checkin_date'],
'checkout' => $tripData['checkout_date'],
'guests' => $tripData['passengers']
], $sagaId);
$this->logStep('hotel_reserved', $hotel);
$car = $this->executeStep('car', 'reserve', [
'city' => $tripData['to'],
'pickup_date' => $tripData['checkin_date'],
'return_date' => $tripData['checkout_date']
], $sagaId);
$this->logStep('car_reserved', $car);
return [
'success' => true,
'booking' => [
'saga_id' => $sagaId,
'flight' => $flight,
'hotel' => $hotel,
'car' => $car
]
];
} catch (Exception $e) {
echo "Booking error: {$e->getMessage()}\n";
echo "Starting compensations...\n";
$this->compensate($sagaId);
throw new Exception("Trip booking failed: " . $e->getMessage());
}
}
private function executeStep(string $service, string $action, array $data, string $sagaId): array
{
$command = [
'saga_id' => $sagaId,
'action' => $action,
'data' => $data,
'timestamp' => time()
];
$message = new AMQPMessage(
json_encode($command),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->channel->basic_publish($message, '', "{$service}_commands");
// Wait for response with timeout
$response = $this->waitForResponse($sagaId, $service, 30);
if ($response['status'] === 'error') {
throw new Exception("Step {$service}/{$action} failed: " . $response['data']);
}
return $response['data'];
}
private function compensate(string $sagaId): void
{
// Execute compensations in reverse order
$reversedLog = array_reverse($this->sagaLog);
foreach ($reversedLog as $step) {
$compensation = $this->getCompensation($step);
if (!$compensation) continue;
$retries = 0;
$maxRetries = 5;
while ($retries < $maxRetries) {
try {
$this->executeStep(
$compensation['service'],
$compensation['action'],
$compensation['data'],
$sagaId
);
echo "Compensated: {$step['type']}\n";
break;
} catch (Exception $e) {
$retries++;
echo "Compensation retry {$retries}/{$maxRetries}: {$e->getMessage()}\n";
sleep(pow(2, $retries)); // Exponential backoff
}
}
}
}
private function getCompensation(array $step): ?array
{
$compensations = [
'flight_reserved' => [
'service' => 'flight',
'action' => 'compensate_cancel',
'data' => ['id' => $step['data']['id'], 'pnr' => $step['data']['pnr']]
],
'hotel_reserved' => [
'service' => 'hotel',
'action' => 'compensate_cancel',
'data' => ['id' => $step['data']['id']]
],
'car_reserved' => [
'service' => 'car',
'action' => 'compensate_cancel',
'data' => ['id' => $step['data']['id']]
]
];
return $compensations[$step['type']] ?? null;
}
private function logStep(string $type, array $data): void
{
$this->sagaLog[] = ['type' => $type, 'data' => $data, 'timestamp' => time()];
}De forma simplificada, trago um único exemplo de como ficaria um dos serviços (FlightService).
class FlightServiceWorker
{
private $channel;
public function __construct()
{
$this->channel = RabbitMQConnection::getChannel();
$this->channel->queue_declare('flight_commands', false, true, false, false);
}
public function start(): void
{
$callback = function($msg) {
$command = json_decode($msg->body, true);
try {
if ($command['is_compensation'] ?? false) {
$result = $this->handleCompensation($command);
} else {
$result = $this->handleCommand($command);
}
$this->sendResponse($command['saga_id'], 'success', $result);
$msg->ack();
} catch (Exception $e) {
$this->sendResponse($command['saga_id'], 'error', $e->getMessage());
$msg->nack(false, true); // Requeue on error
}
};
$this->channel->basic_qos(null, 1, null);
$this->channel->basic_consume('flight_commands', '', false, false, false, false, $callback);
while ($this->channel->is_consuming()) {
$this->channel->wait();
}
}
private function handleCommand(array $command): array
{
switch($command['action']) {
case 'reserve':
return $this->reserveFlight($command['data']);
default:
throw new Exception("Unknown action: {$command['action']}");
}
}
private function handleCompensation(array $command): array
{
switch($command['action']) {
case 'compensate_cancel':
return $this->cancelFlight($command['data']);
default:
throw new Exception("Unknown compensation: {$command['action']}");
}
}
private function reserveFlight(array $data): array
{
// Idempotence
$existingReservation = $this->checkExistingReservation($data);
if ($existingReservation) {
echo "Reservation already exists (idempotence), return...\n";
return $existingReservation;
}
if (rand(1, 100) <= 10) {
throw new Exception("Flight unavailable for the requested date.");
}
$reservationId = uniqid('FLT_');
$pnr = strtoupper(substr(md5($reservationId), 0, 6));
DB::insert('flight_reservations', [
//...
]);
echo "Flight booked: {$pnr} - {$data['from']} → {$data['to']}\n";
return [
'id' => $reservationId,
'pnr' => $pnr,
'from' => $data['from'],
'to' => $data['to'],
'date' => $data['date'],
'passengers' => $data['passengers'],
'status' => 'confirmed',
'price' => 450.00
];
}
private function cancelFlight(array $data): array
{
// Check if it has already been cancelled
$reservation = DB::selectOne('flight_reservations', ['id' => $data['id']]);
if ($reservation['status'] === 'cancelled') { return $reservation; }
DB::update('flight_reservations',
['status' => 'cancelled', 'cancelled_at' => date('Y-m-d H:i:s')],
['id' => $data['id']]
);
return [
'id' => $data['id'],
'pnr' => $data['pnr'],
'status' => 'cancelled'
];
}
private function checkExistingReservation(array $data): ?array
{
$hash = md5(json_encode($data));
return DB::selectOne('flight_reservations', ['idempotency_key' => $hash]) ?: null;
}
private function sendResponse(string $sagaId, string $status, $data): void
{
$response = [
'saga_id' => $sagaId,
'status' => $status,
'data' => $data,
'service' => 'flight',
'timestamp' => time()
];
$message = new AMQPMessage(
json_encode($response),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$this->channel->basic_publish($message, '', 'saga_responses');
}
}
$worker = new FlightServiceWorker();
$worker->start();E para usar o orquestrador (TravelBookingSagaOrchestrator):
try {
$orchestrator = new TravelBookingSagaOrchestrator();
$tripData = [
'from' => 'GRU',
'to' => 'MIA',
'departure_date' => '2026-07-15',
'checkin_date' => '2026-07-15',
'checkout_date' => '2026-07-22',
'passengers' => 2
];
$result = $orchestrator->bookTrip($tripData);
echo "\n Trip booked successfully!\n";
echo "Flight: {$result['booking']['flight']['pnr']}\n";
echo "Hotel: {$result['booking']['hotel']['id']}\n";
echo "Car: {$result['booking']['car']['id']}\n";
} catch (Exception $e) {
echo "\n ERROR: {$e->getMessage()}\n";
echo "All reservations have been cancelled.\n";
}Dica importante: em produção, você rodaria cada worker em um processo separado, e o orquestrador poderia ser chamado via API ou por outro worker consumindo uma fila "create_order".
Configurações para produção
Antes de falar das armadilhas, algumas configurações essenciais para ambientes de produção:
1. Filas duráveis
$channel->queue_declare('flight_commands',
false, // passive
true, // durable - Survives RabbitMQ restart
false, // exclusive
false // auto_delete
);2. Mensagens persistentes
$message = new AMQPMessage(
json_encode($data),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);3. Processar 1 mensagem por vez — importante para evitar sobrecarga
$channel->basic_qos(null, 1, null);4. Dead Letter Exchange (DLX) — para mensagens que falharam vezes demais
$args = new AMQPTable([
'x-dead-letter-exchange' => 'dlx_exchange',
'x-dead-letter-routing-key' => 'failed_bookings'
]);
$channel->queue_declare('flight_commands', false, true, false, false, false, $args);Isso é CRUCIAL para não perder mensagens que falharam múltiplas vezes.
APIs de companhias aéreas, hotéis e locadoras costumam ser lentas ou instáveis. Por isso é importante configurar timeouts:
$connection = new AMQPStreamConnection(
'localhost', 5672, 'guest', 'guest', '/',
false, // insist
'AMQPLAIN', // login method
null, // login response
'en_US', // locale
3.0, // connection timeout
3.0 // read/write timeout
);Ponto importante: retry precisa ser implementado nas compensações!
Armadilhas comuns
Ao longo da implementação de uma SAGA, vamos encontrar alguns obstáculos. Cuidado com:
1. Falta de isolamento
Diferente das transações ACID, Sagas não garantem isolamento. Isso significa que outros processos podem ver estados intermediários.
Exemplo prático: um usuário pode ver que o pagamento foi processado, mas o pedido ainda não está confirmado. Você precisa tratar isso na UI.
2. Compensações podem falhar
Se a compensação falha, você fica em um estado inconsistente sem forma de recuperação automática.
Solução: implemente idempotência e retry automático. Suas compensações precisam ser idempotentes (executar N vezes = executar 1 vez) e você precisa de retry até obter sucesso.
3. Depurar é um pesadelo
Especialmente com coreografia. Você vai precisar de:
- Correlation IDs em todo lugar
- Logs estruturados
- Ferramentas de tracing distribuído
- Muita paciência
4. Transações irreversíveis
Alguns casos não podem ser compensados. Se você enviou um e-mail, não dá para "desenviar". Se imprimiu um ticket, não dá para "desimprimir".
Nesses casos, é preciso pensar em compensações alternativas (como enviar um e-mail de cancelamento).
Ferramentas que podem ajudar
Dependendo do cenário, você não precisa reinventar a roda. Existem várias ferramentas/frameworks disponíveis:
- Axon Framework — popular no mundo Spring Boot
- Eventuate — do próprio Chris Richardson
- Temporal — focado em execução durável (muito bom, aliás)
- AWS Step Functions — se você estiver na AWS
O pessoal do Temporal tem um excelente artigo mostrando como eles abstraem toda a complexidade de rastreamento e retry.
Quando NÃO usar Saga
Importante: nem tudo precisa ser uma Saga.
Não use Saga se:
- A transação é local a um único serviço (óbvio)
- Os dados podem ser eventualmente consistentes SEM coordenação
- O custo da complexidade é maior que o benefício
- Você está começando com microsserviços agora (sério, comece simples)
Como o pessoal do Azure coloca bem na documentação: avalie o risco de negócio. Para operações de baixo risco, consistência eventual simples pode ser suficiente.
Lições que aprendi na prática
Depois de implementar Saga em produção, alguns aprendizados:
- Comece com orquestração — é mais fácil de depurar e evoluir
- Invista em observabilidade — você VAI precisar
- Teste as compensações — não descubra os problemas em produção
- Documente o fluxo — diagramas salvam vidas
- Seja pragmático — nem tudo precisa ser transacional
Conclusão
O Saga Pattern não é bala de prata. Ele adiciona complexidade, exige disciplina e vai fazer você pensar MUITO em casos de borda. Mas quando você realmente precisa de consistência entre múltiplos serviços, ele é praticamente inevitável.
Depois que você entende os conceitos básicos e escolhe a abordagem certa (orquestração vs coreografia), fica mais gerenciável. E existem várias ferramentas que ajudam.
O segredo é não tentar implementar tudo de uma vez. Comece simples, adicione observabilidade desde o início e evolua conforme a necessidade.
Referências
- Microservices.io — Saga Pattern (Chris Richardson)
- Microsoft Learn — Saga Design Pattern
- Baeldung — Saga Pattern in Microservices
- Temporal — Saga Pattern Made Easy
- AWS Prescriptive Guidance — Saga Pattern
- TheServerSide — How the Saga Design Pattern Works