> 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/quickstart/design-patterns/saga.md).

# SAGA

Quando um fluxo de negócio precisa coordenar várias operações que tocam sistemas diferentes — cada um com seu banco, sua API, seu jeito de falhar — uma transação ACID não resolve mais. O padrão SAGA substitui 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, você executa as compensações das etapas anteriores, na ordem inversa, para devolver o sistema a um estado consistente.

O papel natural num SAGA é o de orquestrador: alguém que 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 concluiu. É exatamente o que uma `[SkailFunction]` faz.

### Checkout com compensação

Um fluxo típico de e-commerce ao finalizar a compra: reservar os itens no estoque, cobrar o cartão, emitir a nota fiscal, agendar a coleta com a transportadora. Se qualquer uma dessas etapas falhar, as anteriores precisam ser desfeitas — estoque liberado, pagamento estornado, nota cancelada.

```csharp
[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)
    {
        while (compensacoes.Count > 0)
            await compensacoes.Pop()();

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

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` é executada. Se a emissão da nota falhar depois do pagamento, `EstornarPagamento` e `LiberarItens` rodam, nessa ordem. Se tudo der certo, a pilha simplesmente é descartada ao final.

Cada etapa — `ReservarItens`, `CobrarCartao`, `EmitirNotaFiscal`, `AgendarColeta` — e cada compensação — `LiberarItens`, `EstornarPagamento`, `CancelarNotaFiscal` — é um `[SkailCommand]`. Isso é o que dá ao SAGA sua garantia principal: mesmo que a orquestração seja retomada várias vezes ao longo de dias, cada comando externo é executado exatamente uma vez. Sem isso, uma retomada depois de uma falha poderia cobrar o cartão duas vezes ou gerar duas notas fiscais.

### SAGA com etapas assíncronas

Frameworks tradicionais de SAGA assumem que cada etapa é rápida — milissegundos, segundos no máximo. Quando uma etapa envolve esperar a resposta de um sistema externo que demora horas, ou a aprovação de uma pessoa, esses frameworks ficam desconfortáveis: viram máquinas de estado, tabelas de controle, jobs agendados para verificar se a espera já passou.

Na skail, uma etapa assíncrona é só mais um `await` no meio da função. Um exemplo de emissão de apólice de seguro:

```csharp
[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 = await SkailTask.WaitForEvent<DecisaoSubscritor>(
            "SUBSCRITOR_DECIDIU",
            propostaId.ToString()
        );

        if (!decisao.Aprovada)
            throw new SubscritorRecusouException(decisao.Motivo);

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

        await RegistrarApoliceNaSeguradora(propostaId, cotacao, decisao);
    }
    catch (Exception ex)
    {
        while (compensacoes.Count > 0)
            await compensacoes.Pop()();

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

Entre `SolicitarAprovacaoSubscritor` e o `WaitForEvent`, a execução pode passar horas ou dias hibernada, esperando o subscritor decidir. Do ponto de vista da SAGA, nada muda: a pilha de compensações está intacta, 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 bem maior entre o início e o rollback.

### Retry antes de compensar

Nem toda falha merece compensação imediata. Se a falha é transitória — instabilidade de rede, timeout num parceiro, lock momentâneo — faz mais sentido tentar de novo antes de desfazer tudo o que já foi feito. Um padrão útil é embrulhar a etapa crítica num pequeno loop com backoff e só cair na compensação se o retry não resolver:

```csharp
async SkailTask<bool> TentarComRetry(Func<SkailTask> acao)
{
    var backoff = new[] { TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(2) };

    try { await acao(); return true; }
    catch (FalhaTransienteException) { }

    foreach (var espera in backoff)
    {
        await SkailTask.Delay(espera);
        try { await acao(); return true; }
        catch (FalhaTransienteException) { }
    }

    return false;
}

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

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

        if (!await TentarComRetry(() => CobrarCartao(pedidoId)))
            throw new EtapaNaoResolvidaException("Cobrança esgotou retentativas");

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

        // demais etapas...
    }
    catch (Exception ex)
    {
        while (compensacoes.Count > 0)
            await compensacoes.Pop()();

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

A distinção fica explícita no código: `FalhaTransienteException` é motivo para tentar de novo; qualquer outra exceção é motivo para compensar.

### Observabilidade

No Monitor do portal do skail, uma execução de SAGA aparece como uma única função ativa, com cada `[SkailCommand]` listado em ordem. Em caso de compensação, você vê a sequência exata do rollback — qual etapa falhou, quais compensações rodaram e em que ordem. Isso é especialmente útil para SAGAs longos, onde a janela entre o início do fluxo e o momento da falha pode ser de vários dias.

E porque cada passo e cada compensação é memoizado, uma retomada da execução depois de uma queda de infraestrutura não corre o risco de repetir o que já foi feito nem de pular o que ainda falta.
