Padrões de Resiliência em C#: Explorando Polly, Circuit Breaker e Idempotência
Polly
O Polly é uma biblioteca popular no ecossistema .NET que ajuda a implementar políticas de resiliência e tolerância a falhas em sistemas, permitindo que os erros sejam tratados de forma mais inteligente. Com o Polly, podemos aplicar as seguintes estratégias:

Retry (Repetir)
- Permite repetir uma operação quando ocorre uma falha temporária. Por exemplo, se uma API retornar um erro
5.x.x
, o Polly poderá realizar uma nova tentativa após um intervalo definido.
Circuit Breaker (Disjuntor)
- Monitora falhas sucessivas e "abre o circuito" para evitar sobrecarregar um serviço que já está com problemas. Após um intervalo definido, o circuito "fecha", e novas tentativas são realizadas. É útil para serviços instáveis. Essa estratégia funciona como um disjuntor que, ao identificar uma sobrecarga na rede elétrica da sua casa, desarma para evitar danos aos equipamentos.
Hedging
- Envia solicitações redundantes para diversas instâncias de um serviço com o objetivo de obter a resposta mais rápida e as demais são descartadas.
Timeout
- Refere-se a um tempo máximo que uma solicitação pode levar até ser concluída.
Rate Limiter (Limitador de taxa)
- Controla o número de solicitações que podemos realizar em um determinado tempo. Isso nos ajuda a evitar que sobrecarregamos o serviço que estamos utilizando.
Fallback
- É uma abordagem que define uma ação secundária a ser tomada quando a operação principal falha. Isso pode incluir retornar valores padrão, usar dados em cache ou redirecionar a solicitação para um serviço alternativo.
Idempotência
Idempotência é um conceito da matemática e da ciência da computação que se refere à possibilidade de uma operação ser aplicada várias vezes sem que o resultado se altere.
Quando falamos sobre idempotência em API’s REST, estamos dizendo sobre os métodos HTTP. Mas o que é um método HTTP idempotente? Segundo a documentação da Mozilla
Um método HTTP é idempotente se o efeito pretendido no servidor de fazer uma única solicitação for o mesmo que o efeito de fazer várias solicitações idênticas.
De forma resumida, é a capacidade de uma operação em uma API ser executada várias vezes e produzir sempre o mesmo resultado.
Verbos HTTP

- GET é idempotente porque sempre irá retornar o mesmo recurso e também não altera nenhum estado no servidor.
- PUT é idempotente porque a cada solicitação feita, sempre os mesmo dados serão alterados.
- DELETE é idempotente porque, ao realizarmos a primeira chamada do método e removermos o dado, retornaremos um status code 200 ou 204 e caso realizarmos a segunda chamada, o retorno será o mesmo, pois não existe mais o dado no banco.
- HEAD e OPTIONS são idempotentes.
- O POST não é idempotente porque cada requisição gera uma nova operação, gerando diferentes estados no servidor. Por exemplo, enviar uma requisição POST para criar um novo usuário criará um registro distinto a cada solicitação.
Método HTTP | Idempotente? | Motivo |
---|---|---|
GET | Sim | Não altera o estado do recurso e retorna sempre o mesmo dado. |
PUT | Sim | Sempre atualiza ou cria o recurso com o mesmo estado final. |
DELETE | Sim | Após a remoção inicial, subsequentes requisições não alteram o estado. |
POST | Não | Cada requisição cria um novo recurso ou altera o estado. |
HEAD | Sim | Similar ao GET, mas retorna apenas os cabeçalhos. |
OPTIONS | Sim | Apenas consulta as opções de comunicação com o servidor. |
Implementação
1º - Crie um projeto API Web do ASP.NET Core e salve-o na pasta de sua preferência:

2º - Mantenha as configurações padrão:

3º - Instale o pacote Microsoft.Extensions.Http.Polly em sua API:

Agora, criaremos uma classe para abstrair a injeção de dependência do nosso cliente HTTP e também da configuração do Polly.
Crie a seguinte estrutura em sua API:
📦Api
┣ 📂Extensions
┃ ┣ 📝HttpClients.cs
Adicione o seguinte código ao arquivo HttpClients.cs:
internal static class HttpClients
{
public static void AddClientsFactory(this WebApplicationBuilder builder)
{
const int retryCount = 3;
HttpMethod[] idempotentMethod = { HttpMethod.Get, HttpMethod.Put, HttpMethod.Delete };
builder.Services.AddHttpClient("Api-Externa", client =>
{
client.BaseAddress = new Uri("<https://postman-echo.com>");
}).AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(retryCount, TimeSpan.FromMinutes(1),
onBreak: (outcome, breakDelay) =>
{
Console.WriteLine($"Circuito aberto devido a {retryCount} falhas consecutivas.");
},
onReset: () =>
{
Console.WriteLine("Circuito fechado. Retomando tentativas.");
})).AddPolicyHandler(request =>
{
if (idempotentMethod.Contains(request.Method))
{
return Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} falhou. Tentando novamente em {timespan}.");
});
}
return Policy.NoOpAsync<HttpResponseMessage>();
});
}
}
Em Program.cs, chame o método:
/*...*/
builder.AddClientsFactory();
/*...*/
Logo após adicione a seguinte rota:
app.MapGet("/retry-circuit-breaker", async ([FromServices] IHttpClientFactory httpClientFactory) =>
{
try
{
var client = httpClientFactory.CreateClient("Api-Externa");
var response = await client.GetAsync("/status/500");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return Results.Ok(new
{
Message = "Requisição bem-sucedida!",
Data = content
});
}
else
{
return Results.Json(new
{
Message = "Falha ao processar a requisição.",
StatusCode = response.StatusCode,
}, statusCode: (int)response.StatusCode);
}
}
catch (BrokenCircuitException)
{
return Results.Json(new
{
Message = "Circuito aberto: o serviço está indisponível temporariamente. Por favor, tente novamente mais tarde."
}, statusCode: 503);
}
catch (Exception ex)
{
return Results.Problem(title: "Erro Interno", detail: ex.Message, statusCode: 500);
}
})
.WithName("RetryCircuitBreaker")
.WithOpenApi();
Ao realizar uma requisição, veremos nos logs o retry junto com o circuit-breaker atuando no caso de falha.

Explicação do código
Configurando o cliente HTTP
builder.Services.AddHttpClient("Api-Externa", client =>
{
client.BaseAddress = new Uri("https://postman-echo.com");
})
No trecho de código acima, começamos a definir nosso cliente HTTP que será nomeado como Api-Externa e também a URL que vamos usar. A API do Postman fornece para nós algumas rotas que podemos usar para testes. Segue o link da documentação:
Configurando o Retry
.AddPolicyHandler(request =>
{
if (idempotentMethod.Contains(request.Method))
{
return Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} falhou. Tentando novamente em {timespan}.");
});
}
return Policy.NoOpAsync<HttpResponseMessage>();
})
Aqui começamos a configurar o retry. No método WaitAndRetryAsync definimos a quantidade de repetição que será realizada até o circuito abrir e também o tempo que será feito as retentativas. Neste caso foi definido um tempo exponencial que vai começar com 2 segundos depois 4 e assim sucessivamente. Essa abordagem é chamada de backoff exponencial.
E qual é a intenção de adicionar essa validação de métodos idempotentes?
if (idempotentMethod.Contains(request.Method)){
//...
A ideia é que o retry seja executado apenas em métodos que são idempotentes, pois imagine executando uma retentativa em um POST, a requisição chega até a API, ela processa 50% e lança uma exception, o retry imediatamente irá enviar uma nova requisição e isso pode gerar efeitos colaterais criando dados duplicados. Abaixo há um exemplo de como seria o efeito colateral:
// POST sem idempotência
POST /orders
{
"id": 1,
"produto": "Notebook",
"quantidade": 1
}
// Resultado:
{
"orderId": 101
}
// Retry automático sem idempotência gera:
{
"orderId": 102 // Pedido duplicado
}
Lembre-se que estamos tratando essa situação como um consumidor e não como o fornecedor da API. Existe uma abordagem para métodos que não são idempotentes que seria o cliente enviar uma chave identificando a requisição e a API que está sendo consumida tratar da forma que independente das request solicitadas não tenha efeitos colaterais como duplicação. Fique a vontade para pesquisar por Idempotency-Key.
Configurando o circuit-breaker
Já nesse trecho de código abaixo, vamos configurar o circuit-breaker. No método CircuitBreakerAsync definimos após quantas retentativas o circuito vai abrir e por quanto tempo ficará aberto. Aqui, é importante observar que o circuit-breaker tem três estados:
- Aberto
- Meio aberto
- Fechado
Quando passa do tempo definido na configuração, ele sai do estado fechado para meio aberto que significa que ele irá liberar parcialmente as requisições e se caso alguma retornar sucesso ele troca o estado para aberto:
AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.CircuitBreakerAsync(retryCount, TimeSpan.FromMinutes(1),
onBreak: (outcome, breakDelay) =>
{
Console.WriteLine($"Circuito aberto devido a {retryCount} falhas consecutivas.");
},
onReset: () =>
{
Console.WriteLine("Circuito fechado. Retomando tentativas.");
}));
}
Resumo
- Polly nos fornece ferramentas para lidar com erros transientes e também pode nos causar sérios problemas caso não tenhamos em consciência a idempotência dos métodos.
- Métodos idempotentes garante que o estado final do recurso seja o mesmo após múltiplas requisições.
- Circuit-breaker tem 3 estados: aberto, meio-aberto e fechado.
- Quando dizemos que o circuito está aberto, significa que estamos impedido de realizar novas request, quando está fechado, estamos liberado realizar uma nova tentativa.

Referências

