Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Saga Pattern no Node.js

Atualizado em: 25 de agosto de 2026

Rack de servidores processando fluxos de dados no Node.js

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

  1. Criar pedido pendente.
  2. Reservar estoque.
  3. Autorizar pagamento.
  4. Criar entrega.
  5. 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.created

Nã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/status

Retorne 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/abc123

Cancelamento 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita