O Saga Pattern no Node.js coordena uma operação de negócio que atravessa vários serviços sem depender de uma transação distribuída global. Cada etapa executa uma transação local e, caso uma etapa posterior falhe, ações compensatórias tentam desfazer ou neutralizar os efeitos anteriores.
Em um pedido, por exemplo, a aplicação pode reservar estoque, autorizar pagamento, criar entrega e enviar confirmação. Nenhum banco controla todos esses sistemas. Uma saga registra progresso, define comandos, trata retries e mantém compensações explícitas para chegar a um estado consistente.
Neste guia, você aprenderá coreografia, orquestração, estados, compensações, idempotência, outbox, timeouts, retries, observabilidade, testes e recuperação manual.
O que é Saga Pattern?
Uma saga é uma sequência de transações locais. A referência Saga no catálogo Microservices.io descreve o padrão e suas variações. O artigo acadêmico Sagas, de Hector Garcia-Molina e Kenneth Salem apresenta o conceito original.
Para publicar eventos junto com o banco, consulte Outbox Pattern no Node.js. Para evitar repetição de efeitos, veja Idempotência em APIs Node.js.
Por que não usar uma transação única?
PostgreSQL consegue garantir atomicidade dentro do próprio banco, mas não controla diretamente um provedor de pagamento, serviço de entrega e broker. Protocolos de commit distribuído aumentam acoplamento e nem sempre são suportados.
Exemplo de pedido
- Criar pedido pendente.
- Reservar estoque.
- Autorizar pagamento.
- Criar entrega.
- Confirmar pedido.
Se a entrega falha, a saga pode cancelar a autorização e liberar o estoque.
Compensação não é rollback técnico
Uma compensação é outra operação de negócio. Um pagamento autorizado pode ser cancelado; um pagamento já capturado talvez precise de estorno. O mundo externo pode ter observado o efeito original.
Estados da saga
CREATE TABLE order_sagas (
id UUID PRIMARY KEY,
order_id BIGINT NOT NULL,
state TEXT NOT NULL,
current_step TEXT NOT NULL,
data JSONB NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
next_attempt_at TIMESTAMPTZ,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);Persistir o estado permite retomar após reinício.
Máquina de estados
const transitions = {
CREATED: ['INVENTORY_RESERVED', 'FAILED'],
INVENTORY_RESERVED: ['PAYMENT_AUTHORIZED', 'COMPENSATING'],
PAYMENT_AUTHORIZED: ['DELIVERY_CREATED', 'COMPENSATING'],
DELIVERY_CREATED: ['COMPLETED', 'COMPENSATING'],
COMPENSATING: ['COMPENSATED', 'MANUAL_REVIEW']
};Rejeite transições que não pertencem ao fluxo.
Orquestração
Um orquestrador decide o próximo comando e acompanha respostas:
async function advanceSaga(saga) {
switch (saga.state) {
case 'CREATED':
return reserveInventory(saga);
case 'INVENTORY_RESERVED':
return authorizePayment(saga);
case 'PAYMENT_AUTHORIZED':
return createDelivery(saga);
case 'DELIVERY_CREATED':
return completeOrder(saga);
default:
return saga;
}
}A lógica central facilita visualizar o processo.
Coreografia
Na coreografia, serviços reagem a eventos:
order.created → inventory.reserved
inventory.reserved → payment.authorized
payment.authorized → delivery.createdNão existe um coordenador central, mas o fluxo pode ficar difícil de entender quando cresce.
Orquestração versus coreografia
- Orquestração: fluxo explícito, estado central e compensação coordenada.
- Coreografia: menor dependência de um coordenador, porém mais acoplamento por eventos.
Operações longas e críticas geralmente se beneficiam de orquestração.
Comandos e eventos
Comando pede uma ação: ReserveInventory. Evento informa algo ocorrido: InventoryReserved. Não use um evento como comando disfarçado sem documentar a semântica.
Outbox no orquestrador
Ao atualizar o estado e enviar o próximo comando, grave ambos na mesma transação:
await withTransaction(pool, async client => {
await updateSagaState(client, sagaId, 'RESERVING_INVENTORY');
await insertOutboxEvent(client, {
type: 'inventory.reserve.requested',
aggregateId: sagaId,
payload: { orderId, items }
});
});Inbox no consumidor
O serviço que recebe o comando registra message ID e efeito na mesma transação. Isso impede processar a mesma entrega duas vezes.
Idempotência por etapa
Cada comando precisa de uma chave estável, como sagaId:stepName. Retries reutilizam a mesma chave.
Resposta idempotente
Se ReserveInventory chega novamente, o serviço deve retornar a reserva existente, não criar outra.
Compensações
const compensations = {
DELIVERY_CREATED: cancelDelivery,
PAYMENT_AUTHORIZED: cancelPaymentAuthorization,
INVENTORY_RESERVED: releaseInventory
};Execute em ordem inversa dos efeitos confirmados.
Compensação pode falhar
Um provedor pode estar indisponível durante o estorno. A saga precisa persistir estado, repetir e alertar.
Compensação idempotente
releaseInventory pode chegar várias vezes. Use a mesma chave e registre que a reserva já foi liberada.
Compensação sem inverso perfeito
Um e-mail enviado não pode ser apagado. A compensação pode enviar uma correção. Uma entrega já despachada pode exigir processo humano.
Pivot transaction
Algumas sagas possuem um ponto após o qual o fluxo deve continuar até conclusão, em vez de voltar. Antes do pivot, etapas são compensáveis; depois, etapas precisam de retries confiáveis.
Timeout por etapa
next_attempt_at = now() + interval '30 seconds'Se a resposta não chega, o orquestrador verifica status ou repete o comando.
Timeout não significa falha
O serviço pode ter executado e a resposta se perdido. Consulte por idempotency key antes de compensar.
Retries
Use backoff com jitter e limite:
function retryDelay(attempt) {
return Math.min(300000, 1000 * 2 ** attempt)
+ Math.random() * 500;
}Veja Retry com Backoff no Node.js.
Erros transitórios e permanentes
Timeout e 503 podem ser transitórios. Validação inválida, item inexistente e autorização negada geralmente exigem compensação ou rejeição.
Concorrência no orquestrador
Dois workers podem tentar avançar a mesma saga. Use lock otimista ou pessimista.
Consulte Lock Otimista no Node.js e Lock Pessimista no Node.js.
Update condicional
UPDATE order_sagas
SET state = $1,
version = version + 1,
updated_at = now()
WHERE id = $2
AND version = $3
RETURNING *;Se nenhuma linha retorna, outro worker avançou a saga.
SKIP LOCKED para workers
SELECT id
FROM order_sagas
WHERE next_attempt_at <= now()
AND state NOT IN ('COMPLETED', 'COMPENSATED')
ORDER BY next_attempt_at
FOR UPDATE SKIP LOCKED
LIMIT 20;Consistência eventual
Durante a saga, diferentes serviços observam estados intermediários. A interface deve mostrar “processando” e não prometer conclusão imediata.
Status da operação
GET /api/orders/842/statusRetorne estado, etapa atual e orientação, sem expor erros internos.
202 Accepted
Operações longas podem responder 202 com URL para consulta:
HTTP/1.1 202 Accepted
Location: /api/operations/abc123Cancelamento pelo usuário
Cancelar uma saga depende do estado. Antes do pagamento, talvez seja simples; depois do despacho, pode ser impossível. Modele transições permitidas.
Evento fora de ordem
Inclua versão da saga e rejeite respostas antigas. Um PaymentAuthorized atrasado não deve reativar uma saga já compensada.
Eventos duplicados
Use message ID e inbox. Entrega exatamente uma vez não deve ser assumida.
Eventos órfãos
Uma resposta para saga inexistente ou encerrada deve ser registrada e ignorada ou enviada para análise.
Schema de mensagens
{
"messageId": "...",
"sagaId": "...",
"step": "authorize-payment",
"attempt": 2,
"type": "payment.authorize.requested",
"data": {}
}Versionamento
Durante deploy, instâncias antigas e novas podem processar a mesma saga. Mantenha schema compatível e registre versão do workflow.
Migration de saga ativa
Não altere a máquina de estados sem estratégia para instâncias já em andamento. Mantenha handlers antigos ou migre os registros.
Feature flags
Uma flag pode direcionar novas sagas para a versão nova, enquanto as antigas terminam no fluxo anterior.
Veja Feature Flags no Node.js.
Banco por serviço
Cada serviço mantém sua transação local. O orquestrador não consulta tabelas internas de outros serviços como atalho.
Observabilidade
Registre saga ID, order ID, estado anterior, novo estado, etapa, tentativa, duração e resultado.
Correlation ID
Propague saga ID em logs, traces e mensagens. Isso permite reconstruir o fluxo.
Métricas
Monitore:
- sagas iniciadas;
- concluídas;
- compensadas;
- em revisão manual;
- duração total;
- duração por etapa;
- retries;
- timeouts;
- idade da saga mais antiga.
Consulte Métricas Prometheus no Node.js.
Tracing
Crie spans para cada comando e compensação. Propague contexto no broker. Consulte OpenTelemetry no Node.js.
Alertas
Alerte quando a idade ultrapassa o SLO, compensações falham ou a taxa de manual review cresce.
Revisão manual
Algumas falhas exigem intervenção. Registre ações disponíveis, dados seguros e histórico. Não permita editar estado diretamente sem auditoria.
Runbook
Documente como reenviar comando, consultar provedor, compensar, marcar concluído e escalar o incidente.
Auditoria
Mantenha histórico imutável de transições e ações humanas. O estado atual sozinho não explica como a saga chegou ali.
Testes unitários
Teste a máquina de estados com eventos e falhas determinísticos.
Testes de integração
Use banco e broker reais ou containers. Simule redelivery, atraso e indisponibilidade.
Teste de queda
Encerre o orquestrador depois de gravar a outbox e antes de publicar. Confirme retomada.
Teste de resposta perdida
Execute a etapa, descarte a resposta e repita o comando. Confirme resultado idempotente.
Teste de compensação
test('libera estoque quando pagamento falha', async () => {
paymentProvider.failNextAuthorization();
const saga = await startOrderSaga(input);
await waitUntilFinished(saga.id);
assert.equal(await inventory.isReserved(input.itemId), false);
assert.equal((await findSaga(saga.id)).state, 'COMPENSATED');
});Teste de evento fora de ordem
Envie uma resposta antiga depois da compensação e confirme que o estado não regride.
Erros comuns
- Compensação tratada como rollback: efeitos externos já foram observados.
- Estado só em memória: reinício perde o fluxo.
- Sem idempotência: retries duplicam reservas e cobranças.
- Timeout tratado como falha certa: operação já executada é compensada incorretamente.
- Eventos sem versão: respostas antigas alteram estado.
- Coreografia excessiva: fluxo fica invisível.
- Sem manual review: compensações impossíveis ficam presas.
Boas práticas
- Persista o estado.
- Modele transições explícitas.
- Use outbox e inbox.
- Torne etapas e compensações idempotentes.
- Defina timeout e retry por etapa.
- Diferencie erro transitório e permanente.
- Controle concorrência.
- Versione o workflow.
- Monitore idade e compensações.
- Prepare revisão manual.
Conclusão
O Saga Pattern no Node.js coordena operações distribuídas sem exigir uma transação global. Cada serviço confirma localmente, enquanto o workflow registra progresso e executa compensações quando necessário.
A robustez depende de estado persistente, mensagens confiáveis, idempotência e tratamento de ambiguidades. Com orquestração clara, outbox, métricas e revisão manual, sagas permitem construir fluxos longos que sobrevivem a falhas sem esconder a complexidade do mundo distribuído.




