> 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/fan-out-fan-in.md).

# Fan-out / Fan-in

Disparar N operações ao mesmo tempo (fan-out) e juntar o resultado quando todas terminarem (fan-in). No skail, cada operação é uma `[SkailFunction]` ou um `[SkailCommand]` chamado dentro de um `WhenAll`, com a própria vida durável.

## Quando usar

Fazer a mesma coisa para uma lista de entidades (emitir 5.000 faturas), coletar informação de fontes independentes (fechar o caixa de 50 lojas), processar um lote grande em pedaços. Em código comum o padrão esbarra em threads ocupadas, timeouts e fragilidade quando uma operação falha no meio; aqui cada operação tem o próprio estado, o próprio retry e a própria entrada no Monitor, e o orquestrador hiberna enquanto espera.

## Processamento em lote

```csharp
[SkailFunction]
public async SkailTask ProcessarLoteDeFaturas(Guid loteId)
{
    var faturaIds = await CarregarIdsDoLote(loteId);                 // command: devolve só os ids

    var execucoes = faturaIds.Select(EmitirFaturaIndividual).ToArray();
    await SkailTask.WhenAll(execucoes);

    await ConsolidarResultado(loteId);                               // command
}

[SkailFunction]
public async SkailTask EmitirFaturaIndividual(Guid faturaId)
{
    await GerarDocumentoFiscal(faturaId);                            // commands
    await EnviarParaCliente(faturaId);
    await RegistrarEnvio(faturaId);
}
```

Cada fatura vira uma function filha, com o próprio estado, os próprios pontos de hibernação e a própria linha do tempo. O `WhenAll` coordena o fim: `ConsolidarResultado` só roda depois que todas terminaram. Entre o fan-out e o fan-in, o orquestrador está hibernado e não ocupa worker; podem ser minutos ou horas.

## Coletando resultados

Quando o orquestrador precisa do resultado de cada operação (relatório, falhas parciais), cada filha devolve um valor e o fan-in os coleta pela posição:

```csharp
public record ResultadoFatura(Guid FaturaId, bool Sucesso, string? Erro);

[SkailFunction]
public async SkailTask<RelatorioLote> ProcessarLoteDeFaturas(Guid loteId)
{
    var faturaIds = await CarregarIdsDoLote(loteId);

    var execucoes = faturaIds.Select(EmitirFaturaIndividual).ToArray();
    await SkailTask.WhenAll(execucoes);

    var resultados = new List<ResultadoFatura>();
    foreach (var execucao in execucoes)                              // mesma ordem dos ids
        resultados.Add(await execucao);

    var relatorio = new RelatorioLote(
        Total: resultados.Count,
        Sucesso: resultados.Count(r => r.Sucesso),
        Falhas: resultados.Where(r => !r.Sucesso).ToList());

    await PersistirRelatorio(loteId, relatorio);
    return relatorio;
}

[SkailFunction]
public async SkailTask<ResultadoFatura> EmitirFaturaIndividual(Guid faturaId)
{
    try
    {
        await GerarDocumentoFiscal(faturaId);
        await EnviarParaCliente(faturaId);
        return new ResultadoFatura(faturaId, true, null);
    }
    catch (Exception ex) when (ex is not SkailNonDeterministicException)
    {
        return new ResultadoFatura(faturaId, false, ex.Message);     // a exceção já esgotou o retryCount do command
    }
}
```

Capturar dentro de cada filha e devolver um resultado estruturado impede que uma falha isolada derrube o lote: o `WhenAll` só propaga exceção se alguma filha lançar. A exceção que chega ao `catch` é a final, depois de o runtime ter retentado o command; não escreva laço de retry aqui.

## Fan-out em ondas

Dez mil execuções simultâneas podem saturar o sistema de destino (gateway, ERP, banco) mesmo que o skail dê conta. Processe em janelas:

```csharp
[SkailFunction]
public async SkailTask ProcessarLoteGrande(Guid loteId)
{
    var faturaIds = await CarregarIdsDoLote(loteId);

    foreach (var janela in faturaIds.Chunk(100))
    {
        var execucoes = janela.Select(EmitirFaturaIndividual).ToArray();
        await SkailTask.WhenAll(execucoes);
    }

    await ConsolidarResultado(loteId);
}
```

Cem por vez, até esgotar. `Chunk` sobre uma lista que veio de um command é determinístico: a mesma lista volta do histórico no replay. Ajuste a janela à capacidade do destino. Para lotes muito grandes, lembre que cada filha adiciona passos ao histórico do orquestrador; ver [Limites e cotas](/aprender/garantias/limites-e-cotas.md).

## Fontes independentes

O mesmo padrão para coletar de origens distintas: o fechamento diário de uma rede de 50 lojas, em que cada loja tem o próprio ritmo e o consolidado só faz sentido quando todas responderam.

```csharp
[SkailFunction]
public async SkailTask<FechamentoRede> FechamentoDiario(DateOnly dia)
{
    var lojaIds = await ListarLojasAtivas();

    var fechamentos = lojaIds.Select(id => FecharCaixaDaLoja(id, dia)).ToArray();
    await SkailTask.WhenAll(fechamentos);

    var resultados = new List<ResultadoFechamento>();
    foreach (var f in fechamentos) resultados.Add(await f);

    var consolidado = ConsolidarFechamentoRede(dia, resultados);    // lógica pura, pode ficar na function
    await PersistirFechamentoRede(consolidado);                      // command
    return consolidado;
}
```

## Observabilidade

Cada filha aparece no Monitor como uma execução própria: quais terminaram, quais estão rodando, quais falharam e por quê. Essa visão, em pipelines de lote tradicionais, exigiria instrumentação manual.

## Próximos passos

[Como executar em paralelo e agregar resultados](/construir/escrever-fluxos/como-executar-em-paralelo-e-agregar-resultados.md) é o guia curto do mecanismo. A referência de [WhenAll e WhenAny](/construir/sdk-.net/whenall-e-whenany.md). [Saga](/construir/padroes-e-antipadroes/saga.md) para quando as operações precisam ser desfeitas em caso de falha.
