> 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/escrever-fluxos/como-escrever-commands-idempotentes.md).

# Como escrever commands idempotentes

O skail garante que um command não executa de novo depois de gravar o resultado. Ele não pode garantir que o efeito externo aconteceu uma única vez se o command falhar entre o efeito e a gravação. Este guia mostra como fechar essa janela com uma chave de negócio, em três situações.

## Por que a janela existe

Um command chama o gateway de pagamento. O gateway cobra e responde 200. Antes de o runtime gravar o resultado no histórico, o processo cai. Na reentrega, o replay chega ao command sem passo gravado e o executa de novo: segunda cobrança. A janela dura milissegundos e é rara, mas em escala ela acontece, e é a mesma em qualquer sistema distribuído. Ver [Falhas, retries e idempotência](/aprender/fundamentos/falhas-retries-e-idempotencia.md).

A solução é sempre a mesma: a chamada externa leva uma chave que identifica a operação de negócio, e o sistema externo reconhece a repetição.

## Caso 1: o sistema externo aceita chave de idempotência

Gateways de pagamento, provedores de e-mail e APIs bem desenhadas aceitam um header ou campo de idempotência. Use o id de negócio da operação, não um valor gerado na hora:

```csharp
[SkailCommand(retryCount: 8)]
public async SkailTask<ResultadoCobranca> CobrarCartao(Guid faturaId, decimal valor)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, "/charges")
    {
        Content = JsonContent.Create(new { amount = valor, invoice = faturaId })
    };
    request.Headers.Add("Idempotency-Key", $"cobranca-{faturaId}");   // mesma chave em toda tentativa

    var resposta = await _gateway.SendAsync(request);
    resposta.EnsureSuccessStatusCode();

    var corpo = await resposta.Content.ReadFromJsonAsync<RespostaGateway>();
    return corpo!.Approved ? ResultadoCobranca.Aprovado : ResultadoCobranca.Recusado;
}
```

A chave é derivada dos argumentos (`faturaId`), então é idêntica em toda tentativa. Um `Guid.NewGuid()` como chave seria diferente a cada tentativa e não protegeria nada. Se a mesma fatura pode ser cobrada mais de uma vez legitimamente (retentativa de negócio dias depois), inclua o número da tentativa na chave: `cobranca-{faturaId}-{tentativa}`, com `tentativa` vindo como argumento.

## Caso 2: o efeito é no seu banco

Escrita no próprio banco: use upsert por chave natural, ou insert com chave única e trate a violação como sucesso.

```csharp
[SkailCommand]
public async SkailTask RegistrarEmissao(Guid pedidoId, string chaveNfe)
{
    await _db.Database.ExecuteSqlInterpolatedAsync($@"
        INSERT INTO emissoes (pedido_id, chave_nfe, emitido_em)
        VALUES ({pedidoId}, {chaveNfe}, now())
        ON CONFLICT (pedido_id) DO NOTHING");
}
```

Repetir o command produz o mesmo estado. Evite `INSERT` puro em tabelas sem chave única e evite `UPDATE saldo = saldo + x`: prefira gravar o fato (`lancamentos` com chave única por operação) e derivar o saldo.

## Caso 3: o sistema externo não aceita chave

Um ERP antigo, um serviço de terceiros sem idempotência. Então a idempotência é sua, com uma tabela de efeitos aplicados, consultada antes e gravada depois, dentro do mesmo command:

```csharp
[SkailCommand(retryCount: 3)]
public async SkailTask<string> EmitirNoErp(Guid pedidoId)
{
    var jaEmitida = await _db.EfeitosAplicados
        .Where(e => e.Chave == $"emissao-erp-{pedidoId}")
        .Select(e => e.Resultado)
        .FirstOrDefaultAsync();
    if (jaEmitida is not null) return jaEmitida;              // já aconteceu: devolve o mesmo resultado

    var numero = await _erp.EmitirAsync(pedidoId);            // o efeito

    _db.EfeitosAplicados.Add(new EfeitoAplicado($"emissao-erp-{pedidoId}", numero));
    await _db.SaveChangesAsync();
    return numero;
}
```

Ainda há uma janela entre `_erp.EmitirAsync` e `SaveChangesAsync`, menor que a original e sob seu controle; se o ERP oferece alguma consulta por referência externa, use-a no lugar da tabela. Sem nenhuma das duas, considere `retryCount: 0` e trate a falha na function, para que a decisão de repetir seja humana.

## retryCount como parte da idempotência

`retryCount` alto (8, 10) para efeitos idempotentes contra integrações instáveis: repetir é seguro e barato. `retryCount: 0` para efeitos que não podem ser repetidos de forma alguma e não têm chave: a exceção chega à function na primeira falha e você decide (compensar, marcar pendente, avisar alguém). Nunca deixe o default sem ter pensado nisso.

## Checklist

* Toda chamada externa com efeito leva uma chave derivada dos argumentos do command.
* Escrita no banco é upsert por chave natural ou insert com chave única.
* Sem chave possível: tabela de efeitos aplicados no mesmo command, ou `retryCount: 0`.
* Falha de negócio é resultado, não exceção; exceção só para o que pode dar certo na próxima tentativa.

## Próximos passos

[Como tratar erros e compensar](/construir/escrever-fluxos/como-tratar-erros-e-compensar.md) para quando a operação não pode ser desfeita e precisa ser compensada. [Saga](/construir/padroes-e-antipadroes/saga.md) para o padrão completo. A referência de [SkailCommand](/construir/sdk-.net/skailcommand.md) para `retryCount`.
