> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/construir/padroes-e-antipadroes/saga.md).

# Saga

Como coordenar várias operações em sistemas diferentes, cada uma com sua compensação, usando uma `[SkailFunction]` como orquestrador e `[SkailCommand]`s memoizados como etapas: se uma etapa falha, as anteriores são desfeitas em ordem inversa, mesmo que o fluxo tenha ficado dias hibernado no meio.

## Quando usar

Quando um fluxo de negócio toca sistemas diferentes (cada um com seu banco, sua API, seu jeito de falhar) e uma transação ACID não é possível. O padrão Saga troca a transação distribuída por uma sequência de operações locais, cada uma com uma contrapartida de compensação. Se uma etapa do meio falha, as compensações das etapas anteriores rodam na ordem inversa e devolvem o sistema a um estado consistente.

O papel natural em uma saga é o do orquestrador: quem sabe quais são as etapas, em que ordem rodam, o que compensar quando algo dá errado e quando considerar que o fluxo como um todo terminou. É exatamente o que uma `[SkailFunction]` faz.

## Checkout com compensação

Um checkout de e-commerce: reservar os itens no estoque, cobrar o cartão, emitir a nota fiscal, agendar a coleta com a transportadora. Se qualquer etapa falhar, as anteriores precisam ser desfeitas: estoque liberado, pagamento estornado, nota cancelada.

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Skail.Platform.Runtime.Standard;
using Skail.Platform.Runtime.Standard.Threading;

public class CheckoutSaga
{
    [SkailFunction]
    public async SkailTask FinalizarCompra(Guid pedidoId)
    {
        var compensacoes = new Stack<Func<SkailTask>>();

        try
        {
            await ReservarItens(pedidoId);
            compensacoes.Push(() => LiberarItens(pedidoId));

            await CobrarCartao(pedidoId);
            compensacoes.Push(() => EstornarPagamento(pedidoId));

            await EmitirNotaFiscal(pedidoId);
            compensacoes.Push(() => CancelarNotaFiscal(pedidoId));

            await AgendarColeta(pedidoId);
        }
        catch (Exception ex) when (ex is not SkailNonDeterministicException)
        {
            while (compensacoes.Count > 0)
                await compensacoes.Pop()();

            await RegistrarCheckoutAbortado(pedidoId, ex.Message);
        }
    }

    // Etapas: cada uma é um command idempotente por pedidoId
    [SkailCommand] public async SkailTask ReservarItens(Guid pedidoId)     { /* serviço de estoque */ }
    [SkailCommand] public async SkailTask CobrarCartao(Guid pedidoId)      { /* gateway de pagamento */ }
    [SkailCommand] public async SkailTask EmitirNotaFiscal(Guid pedidoId)  { /* emissor fiscal */ }
    [SkailCommand] public async SkailTask AgendarColeta(Guid pedidoId)     { /* transportadora */ }

    // Compensações: também commands, também idempotentes
    [SkailCommand] public async SkailTask LiberarItens(Guid pedidoId)      { /* estoque */ }
    [SkailCommand] public async SkailTask EstornarPagamento(Guid pedidoId) { /* gateway */ }
    [SkailCommand] public async SkailTask CancelarNotaFiscal(Guid pedidoId){ /* emissor fiscal */ }
    [SkailCommand] public async SkailTask RegistrarCheckoutAbortado(Guid pedidoId, string motivo) { /* banco */ }
}
```

A pilha de compensações registra o que desfazer logo depois de cada etapa dar certo. Se a cobrança falhar depois da reserva, só `LiberarItens` roda. Se a emissão da nota falhar depois do pagamento, `EstornarPagamento` e `LiberarItens` rodam, nessa ordem. Se tudo der certo, a pilha é descartada ao final. A pilha é uma variável local da function e é reconstruída igual em todo replay, porque só depende da ordem em que os commands concluíram, e essa ordem está no histórico.

Três detalhes desse código que diferem do que se escreve em código comum:

Depois de compensar, a function registra o resultado e termina. Ela não relança a exceção. Se relançasse, seria reentregue até o seu `retryCount` (15 por padrão) e, em cada reentrega, o command que falhou tentaria de novo as suas `retryCount` vezes; as compensações não repetiriam (já estão gravadas), mas o fluxo levaria horas para "falhar" de novo, à toa. Ver [Falhas, retries e idempotência](/aprender/fundamentos/falhas-retries-e-idempotencia.md).

O `catch` exclui `SkailNonDeterministicException`. Ela indica que o código e o histórico divergiram; capturá-la para compensar esconderia o erro. Ver [Determinismo e replay](/aprender/fundamentos/determinismo-e-replay.md).

Cada etapa e cada compensação é um `[SkailCommand]` e recebe o `pedidoId` como chave de idempotência na chamada externa. Isso dá à saga a propriedade que importa: depois que um command grava o resultado, ele não roda de novo naquela execução, mesmo que a orquestração seja retomada várias vezes ao longo de dias. O que o skail não garante é que o efeito externo aconteceu exatamente uma vez: se o command falhar entre o efeito e a gravação, ele roda de novo. A chave de idempotência fecha essa janela do lado do sistema externo. Ver [Como escrever commands idempotentes](/construir/escrever-fluxos/como-escrever-commands-idempotentes.md).

## Saga com etapas assíncronas

Frameworks tradicionais de saga assumem que cada etapa é rápida. Quando uma etapa envolve esperar horas pela resposta de um sistema externo, ou dias pela aprovação de uma pessoa, eles viram máquinas de estado, tabelas de controle e jobs para verificar se a espera passou.

No skail, uma etapa assíncrona é só mais um `await` no meio da function. Emissão de apólice de seguro, com a decisão do subscritor no meio (trecho da mesma classe):

```csharp
private const string SubscritorDecidiu = "SUBSCRITOR_DECIDIU";

[SkailFunction]
public async SkailTask EmitirApolice(Guid propostaId)
{
    var compensacoes = new Stack<Func<SkailTask>>();

    try
    {
        var cotacao = await CalcularPremio(propostaId);
        compensacoes.Push(() => DescartarCotacao(cotacao.Id));

        await SolicitarAprovacaoSubscritor(propostaId, cotacao);

        var decisao = SkailTask.WaitForEvent<DecisaoSubscritor>(SubscritorDecidiu, propostaId.ToString());
        var prazo   = SkailTask.Delay(TimeSpan.FromDays(5));

        var vencedor = await SkailTask.WhenAny(decisao, prazo);
        if (vencedor == prazo)
            throw new TimeoutException("Subscritor não decidiu em 5 dias.");

        var resposta = await decisao;
        if (!resposta.Aprovada)
            throw new InvalidOperationException($"Subscritor recusou: {resposta.Motivo}");

        await CobrarPrimeiraParcela(propostaId, cotacao.Valor);
        compensacoes.Push(() => EstornarPrimeiraParcela(propostaId));

        await RegistrarApoliceNaSeguradora(propostaId, cotacao, resposta);
    }
    catch (Exception ex) when (ex is not SkailNonDeterministicException)
    {
        while (compensacoes.Count > 0)
            await compensacoes.Pop()();

        await RegistrarEmissaoAbortada(propostaId, ex.Message);
    }
}
```

Entre `SolicitarAprovacaoSubscritor` e o `WhenAny`, a execução pode passar dias hibernada. Do ponto de vista da saga, nada muda: a pilha de compensações é reconstruída no replay, e se depois da aprovação algo na cobrança ou no registro falhar, a compensação segue o mesmo caminho, só que com um intervalo maior entre o início e o rollback.

As duas exceções lançadas dentro do `try` (prazo expirado, recusa) são controle de fluxo local: nascem e são capturadas na própria function, então não geram reentrega. Elas existem só para reaproveitar o bloco de compensação. A espera tem prazo porque toda espera deve ter: ver [Timeout e prazo](/construir/padroes-e-antipadroes/timeout-e-prazo.md).

## Retry antes de compensar

Nem toda falha merece compensação imediata. Se a cobrança falhou por instabilidade de rede ou timeout do gateway, faz mais sentido tentar de novo antes de desfazer tudo. No skail você não escreve esse laço: o runtime já reexecuta um command que lançou exceção até o `retryCount` dele, e a exceção só chega à function quando as tentativas esgotam. Um laço manual de `try/catch` com `SkailTask.Delay` dentro da function duplicaria o que o runtime já fez.

O que você faz é dimensionar o `retryCount` do command crítico e tratar a exceção final uma vez, no `catch` que compensa:

```csharp
[SkailCommand(retryCount: 8)]
public async SkailTask<ResultadoCobranca> CobrarCartao(Guid pedidoId)
{
    // gateway de pagamento, com pedidoId como Idempotency-Key
}
```

```csharp
// Trecho de FinalizarCompra
var cobranca = await CobrarCartao(pedidoId);       // exceção só chega aqui depois de 8 tentativas
if (cobranca == ResultadoCobranca.Recusado)
    throw new InvalidOperationException("Cartão recusado.");  // vai para o catch e compensa

compensacoes.Push(() => EstornarPagamento(pedidoId));
```

Dois casos ficam separados no código. Falha transitória (rede, `5xx` do gateway): o command lança, o runtime tenta oito vezes, e se ainda assim falhar a exceção chega ao `catch` e a saga compensa. Falha de negócio (cartão recusado): o command devolve um resultado, sem exceção e sem retentativa, e a function decide compensar. O runtime não classifica exceções em transitórias ou definitivas; é o seu command que faz essa distinção ao devolver resultado em vez de lançar. Ver [Padrões recomendados](/construir/padroes-e-antipadroes/padroes-recomendados.md), regra 4.

Se o seu caso exige espaçamento longo entre as tentativas (esperar minutos até o parceiro voltar), o `retryCount` não é a ferramenta: ele controla quantas vezes, não o espaçamento. Para esse cenário, [Polling de sistema externo](/construir/padroes-e-antipadroes/polling-de-sistema-externo.md) mostra um laço com `SkailTask.Delay` crescente e limite explícito, que é diferente de retry: ali cada consulta é um command que conclui normalmente devolvendo "ainda não".

## Observabilidade

No Monitor, uma execução de saga aparece como uma única function, com cada command listado em ordem. Em caso de compensação, você vê a sequência exata do rollback: qual etapa falhou, quantas tentativas consumiu, quais compensações rodaram e em que ordem. Em sagas longas, com dias entre o início e a falha, essa linha do tempo é o que substitui a reconstrução manual a partir de logs de sistemas diferentes.

Como cada etapa e cada compensação é memoizada, uma retomada depois de uma queda de infraestrutura não repete o que já foi feito nem pula o que ainda falta.

## Próximos passos

[Como tratar erros e compensar](/construir/escrever-fluxos/como-tratar-erros-e-compensar.md) é o guia passo a passo deste padrão. [Falhas, retries e idempotência](/aprender/fundamentos/falhas-retries-e-idempotencia.md) explica as duas camadas de retry que a saga aproveita. [Human-in-the-loop](/construir/padroes-e-antipadroes/human-in-the-loop.md) detalha a etapa de aprovação que apareceu na apólice.
