> 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/construa/como-fazer/padroes-e-antipadroes/padroes-recomendados.md).

# Padrões recomendados

Onze regras que fazem um fluxo skail ser previsível em produção. Cada uma existe por causa de um mecanismo do produto (replay, memoização, retry, correlação de eventos), e a razão está escrita ao lado.

## 1. Decisões na function, operações de I/O no command

A function só decide e encadeia. Toda operação de I/O (HTTP, banco, fila, e-mail) e toda fonte de não determinismo (relógio, aleatoriedade, variável de ambiente) moram em um command. Motivo: o corpo da function é reexecutado a cada retomada; o command, depois de concluído, não roda de novo e o resultado fica gravado. Ver [Determinismo e replay](/construa/conceitos/replay-e-determinismo.md).

{% tabs %}
{% tab title="C#" %}

```csharp
[SkailFunction]
public async SkailTask EmitirNota(Guid pedidoId)
{
    var pedido = await CarregarPedido(pedidoId);     // command
    var xml    = MontarXml(pedido);                   // lógica pura, pode ficar na function
    var chave  = await EnviarParaPrefeitura(xml);     // command
    await GravarChave(pedidoId, chave);               // command
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { SkailCommand, SkailFunction } from "@skail/runtime";

export interface Pedido {
  Id: string;
  Itens: string[];
}

export class NotaService {
  @SkailFunction()
  async EmitirNota(pedidoId: string): Promise<void> {
    const pedido = await this.CarregarPedido(pedidoId); // command
    const xml = this.MontarXml(pedido); // lógica pura, pode ficar na function
    const chave = await this.EnviarParaPrefeitura(xml); // command
    await this.GravarChave(pedidoId, chave); // command
  }

  private MontarXml(pedido: Pedido): string {
    return `<pedido id="${pedido.Id}">${pedido.Itens.map((i) => `<item>${i}</item>`).join("")}</pedido>`;
  }

  @SkailCommand() async CarregarPedido(pedidoId: string): Promise<Pedido> { return { Id: pedidoId, Itens: [] }; }
  @SkailCommand() async EnviarParaPrefeitura(xml: string): Promise<string> { return ""; }
  @SkailCommand() async GravarChave(pedidoId: string, chave: string): Promise<void> {}
}
```

{% endtab %}
{% endtabs %}

## 2. Um command por operação de I/O

Cobrar, emitir e notificar são três commands, não um. Motivo: quando um command falha, o retry reexecuta o command inteiro. Se ele fez três coisas, as duas que deram certo acontecem de novo. E um command não chama outro command nem uma function: quem compõe os passos é sempre a function, que chama um depois do outro quando eles precisam ser duráveis separadamente; lógica comum entre commands fica em métodos comuns, que não são function nem command. Ver a referência de [SkailCommand](/referencia/sdk-csharp/skailcommand.md) no SDK C#.

## 3. Idempotência por chave de negócio dentro do command

Passe o id do pedido, da fatura ou da tentativa como chave de idempotência na chamada externa (header `Idempotency-Key`, campo do payload, ou `UPSERT` por chave no banco). Motivo: o skail garante que o command não roda de novo depois de gravar o resultado, mas se ele falhar entre a operação externa e a gravação, roda de novo. Ver [Como escrever commands idempotentes](/construa/como-fazer/escrever-commands-idempotentes.md).

## 4. Falha de negócio é resultado, não exceção

Cartão recusado, CPF inválido, estoque insuficiente: devolva `Recusado` como resultado e decida na function. Motivo: qualquer exceção em command gera nova tentativa até o `retryCount`; uma recusa definitiva seria retentada cinco vezes à toa. Com o retry ligado, exceção é para o que pode dar certo na próxima tentativa. Se o seu código já sinaliza a falha por exceção, use `retryCount: 0` (regra 9) e trate na function, como em [Como tratar erros e compensar](/construa/como-fazer/tratar-erros-e-desfazer-passos.md).

## 5. instanceId de evento é um id de negócio estável

O segundo argumento da espera por evento e o último segmento do `/api/v1/fire` precisam ser idênticos, e o jeito de garantir isso é usar um id que os dois lados já conhecem: o id da fatura, da solicitação, da conversa. Nunca use um valor gerado na hora.

## 6. Nomes de evento são constantes compartilhadas

{% tabs %}
{% tab title="C#" %}

```csharp
public static class Eventos
{
    public const string PagamentoConfirmado = "PAGAMENTO_CONFIRMADO";
    public const string AprovacaoRecebida   = "APROVACAO_RECEBIDA";
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
export const Eventos = {
  PagamentoConfirmado: "PAGAMENTO_CONFIRMADO",
  AprovacaoRecebida: "APROVACAO_RECEBIDA",
} as const;

```

{% endtab %}
{% endtabs %}

As mesmas constantes entram na espera por evento, dentro da function, e na URL do fire (`/api/v1/fire/...`), no código que dispara. Motivo: um typo em uma string e o evento nunca correlaciona; a execução fica esperando para sempre.

## 7. Toda espera tem prazo

Toda espera por evento vem acompanhada de um delay de prazo: a function espera o evento ou o prazo, o que vier primeiro, e decide o que fazer no timeout. Motivo: um evento que nunca chega (webhook perdido, pessoa que não aprovou) não pode deixar a execução parada indefinidamente sem que ninguém saiba. Ver [Como esperar com timeout](/construa/como-fazer/esperar-um-evento-com-prazo.md).

{% tabs %}
{% tab title="C#" %}

```csharp
var confirmacao = SkailTask.WaitForEvent<ConfirmacaoPagamento>(Eventos.PagamentoConfirmado, faturaId.ToString());
var prazo       = SkailTask.Delay(TimeSpan.FromHours(48));

var vencedor = await SkailTask.WhenAny(confirmacao, prazo);
if (vencedor == prazo) { await CancelarPorExpiracao(faturaId); return; }
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { Skail, SkailCommand, SkailFunction } from "@skail/runtime";

const Eventos = { PagamentoConfirmado: "PAGAMENTO_CONFIRMADO" } as const;

export interface ConfirmacaoPagamento {
  TransacaoId: string;
}

export class FaturaService {
  @SkailFunction()
  async AguardarPagamento(faturaId: string): Promise<void> {
    const confirmacao = Skail.waitForEvent<ConfirmacaoPagamento>(Eventos.PagamentoConfirmado, faturaId);
    const prazo = Skail.delayMinutes(48 * 60);

    const vencedor = await Skail.whenAny(confirmacao, prazo);
    if (vencedor === undefined) {
      await this.CancelarPorExpiracao(faturaId);
      return;
    }
  }

  @SkailCommand() async CancelarPorExpiracao(faturaId: string): Promise<void> {}
}
```

{% endtab %}
{% endtabs %}

## 8. Argumentos e retornos pequenos, serializáveis e imutáveis

Ids e tipos simples, não entidades de ORM, streams, a resposta inteira do cliente HTTP ou listas com milhares de itens. Para dados grandes, passe uma referência (id, URL, chave de blob) e carregue dentro do command que precisa. Motivo: argumentos e retornos são gravados no histórico da execução e desserializados a cada replay; um histórico pesado deixa a retomada lenta.

## 9. retryCount explícito quando o default não serve

`retryCount` 10 no command para integrações sabidamente instáveis; `retryCount` 0 quando a function precisa tratar a exceção na primeira falha (falha de negócio sinalizada por exceção, caminho alternativo como contingência). O zero não dispensa a chave de idempotência: um command interrompido por queda do processo antes de gravar o resultado roda de novo na retomada, com qualquer `retryCount`. Escolha o `retryCount` de acordo com o caso, sem depender do valor padrão. Ver [Falhas, retries e idempotência](/construa/conceitos/retries-e-idempotencia.md).

## 10. Versione antes de mudar o fluxo de uma function com execuções em andamento

Novo método com endereço explícito (`skailMethodName` em C#, opção `uri` em TypeScript), ou nova versão do workload; a versão antiga fica publicada até a última execução dela terminar. Motivo: mudar os `await`s de uma function com histórico gera o erro de não determinismo no replay. Ver [Versionamento](/construa/conceitos/versoes-com-execucoes-em-andamento.md).

## 11. Nomes legíveis para quem olha o Monitor

`EmitirNotaFiscal`, `AguardarAprovacaoDoGerente`, não `Step3` ou `Process2`. Os nomes de function e command são o que aparece na linha do tempo do Monitor e nos spans de rastreamento. Todo log precisa levar o TaskId da execução: em C#, os logs do `ILogger` dentro de functions e commands já saem correlacionados por ele; em TypeScript, inclua `SkailContext.current.taskId` na linha do `console`.

## Checklist rápido

Antes de abrir o pull request de um fluxo skail:

* Nenhum I/O, relógio, id gerado ou número aleatório dentro de uma function.
* Um command por operação de I/O; cada command com chave de idempotência na chamada externa.
* Falhas de negócio como retorno, ou exceção com `retryCount: 0` tratada na function; `retryCount` pensado.
* Toda espera por evento com prazo; nomes de evento em constantes; instanceId de negócio.
* Argumentos e retornos são tipos simples e pequenos.
* Se a function já tem execuções em produção, o fluxo não mudou, ou foi versionado.

A versão completa está em [Checklist de revisão de código skail](/construa/como-fazer/padroes-e-antipadroes/checklist-de-revisao-de-codigo-skail.md).

## Próximos passos

[Antipadrões](/construa/como-fazer/padroes-e-antipadroes/antipadroes-o-que-nao-fazer-e-por-que.md) complementa esta página: o que não fazer, por que quebra e como corrigir. Os padrões de orquestração (Saga, Fan-out/Fan-in, Human-in-the-loop, Debounce/Aggregator, Timeout, Polling, Régua de lembretes) estão no mesmo grupo.
