Skip to main content

Graceful Degradation em Sistemas Modernos | Padrões de Resiliência | Arquitetura

  • Graceful Degradation em Sistemas Modernos | Padrões de Resiliência | Arquitetura

O que é Graceful Degradation

Graceful Degradation é a capacidade de um sistema continuar entregando valor mesmo quando alguma parte dele está degradada, lenta ou indisponível.

A ideia não é impedir todas as falhas.

A ideia é fazer o sistema falhar de forma controlada.

Em vez de:

Banco lento → sistema inteiro fora

Um sistema maduro faz:

Banco lento → desliga funcionalidades secundárias → mantém o core funcionando

Exemplo simples

Imagine um app financeiro.

Se o banco de dados começa a responder lentamente, o sistema pode:

  • bloquear extrato detalhado de 90 dias
  • reduzir extrato para 7 dias
  • desativar recomendações
  • usar cache para saldo aproximado
  • manter PIX e login funcionando
  • rejeitar novas requisições não críticas rapidamente

Isso é Graceful Degradation.


O problema que ele resolve

Em sistemas distribuídos, falhas são esperadas:

  • banco lento
  • Redis fora
  • fila acumulando
  • API terceira instável
  • CPU alta
  • thread pool saturado
  • rede com latência
  • timeout em cascata
  • pico de acesso

O erro comum é tentar manter tudo funcionando a qualquer custo.

O sistema moderno faz o contrário: preserva o essencial e sacrifica o secundário.


Princípio central

Nem toda funcionalidade tem a mesma prioridade.

Um sistema maduro separa funcionalidades em níveis.

NívelTipoExemploDeve continuar?
CríticoCore businessPIX, pagamento, login, saldoSim
ImportanteApoio ao coreextrato recente, notificações importantesPreferencialmente
SecundárioExperiênciarecomendações, dashboard, histórico longoPode cair
PesadoCusto altorelatórios, exportação PDF, analyticsDeve ser desligável

Estratégias comuns de Graceful Degradation

1. Feature Flags

Feature flags permitem ligar ou desligar funcionalidades sem novo deploy.

Exemplo:

{
"FeatureManagement": {
"Pix": true,
"SaldoAtual": true,
"Extrato90Dias": false,
"Recomendacoes": false,
"ExportacaoPdf": false
}
}

Quando o sistema detecta degradação, pode desativar funcionalidades caras.

Exemplo em .NET:

app.MapGet("/extrato/90-dias", async (
IFeatureManager featureManager,
IExtratoService extratoService,
string contaId) =>
{
if (!await featureManager.IsEnabledAsync("Extrato90Dias"))
{
return Results.Problem(
title: "Funcionalidade temporariamente indisponível",
detail: "O extrato de 90 dias foi desativado para preservar operações críticas.",
statusCode: StatusCodes.Status503ServiceUnavailable);
}

var extrato = await extratoService.BuscarAsync(contaId, dias: 90);

return Results.Ok(extrato);
});

Uso comum:

  • desligar relatórios
  • desligar dashboards
  • reduzir consultas caras
  • desativar integrações secundárias
  • controlar rollout de funcionalidades

2. Cache Fallback

Quando a fonte principal falha ou fica lenta, o sistema usa o último dado conhecido.

Fluxo normal:

API → Banco → Resposta

Fluxo degradado:

API → Banco lento/falhou → Redis → Resposta aproximada

Exemplo:

public async Task<SaldoResponse> ObterSaldoAsync(string contaId, CancellationToken ct)
{
try
{
var saldo = await _database.ObterSaldoAsync(contaId, ct);

await _cache.SetAsync(
$"saldo:{contaId}",
saldo,
TimeSpan.FromMinutes(5),
ct);

return new SaldoResponse(
Saldo: saldo,
Fonte: "database",
ModoDegradado: false);
}
catch (TimeoutException)
{
var saldoCache = await _cache.GetAsync<decimal>($"saldo:{contaId}", ct);

return new SaldoResponse(
Saldo: saldoCache,
Fonte: "cache",
ModoDegradado: true);
}
}

Esse padrão é útil quando o dado pode aceitar consistência eventual.

Bons candidatos:

DadoPode usar cache fallback?
saldo exibido em telaSim, com aviso
lista de produtosSim
configurações públicasSim
extrato recenteDepende
confirmação de pagamentoNão recomendado
débito efetivo em contaNão

3. Timeout curto

Sem timeout, uma dependência lenta prende recursos da aplicação.

Exemplo ruim:

API chama banco → banco demora 40s → threads presas → aplicação satura

Exemplo melhor:

API chama banco → timeout em 2s → fallback ou erro controlado

Exemplo com HttpClient:

builder.Services.AddHttpClient<IPagamentoClient, PagamentoClient>(client =>
{
client.Timeout = TimeSpan.FromSeconds(2);
});

Exemplo com CancellationTokenSource:

using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(2));

var resultado = await _servicoExterno.ProcessarAsync(payload, timeout.Token);

Timeout não é detalhe técnico. Timeout é regra arquitetural.


4. Retry com limite

Retry ajuda quando a falha é transitória.

Mas retry mal configurado piora a crise.

Exemplo perigoso:

Banco lento
→ API faz 5 retries
→ carga multiplica
→ banco fica ainda pior

Retry precisa ter:

  • limite de tentativas
  • backoff
  • jitter
  • timeout total
  • escopo bem definido
  • nunca ser infinito

Exemplo com Polly:

builder.Services.AddHttpClient<IClienteExterno, ClienteExterno>()
.AddTransientHttpErrorPolicy(policy =>
policy.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt))));

Uso adequado:

CenárioRetry?
erro de rede transitórioSim
HTTP 500 eventualSim, com limite
timeout por sobrecargaCom cuidado
erro de validaçãoNão
saldo insuficienteNão
dado inválidoNão

5. Circuit Breaker

Circuit Breaker evita insistir em uma dependência quebrada.

Estados básicos:

Closed → tudo normal
Open → chamadas bloqueadas
Half-Open → testa se voltou

Exemplo:

API de antifraude falhando
→ circuito abre
→ sistema para de chamar antifraude por alguns segundos
→ evita saturar a API e a própria aplicação

Exemplo com Polly:

builder.Services.AddHttpClient<IAntifraudeClient, AntifraudeClient>()
.AddTransientHttpErrorPolicy(policy =>
policy.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)));

Quando o circuito abre, você pode:

  • retornar erro controlado
  • usar cache
  • enfileirar processamento
  • reduzir funcionalidade
  • exigir revisão manual

6. Backpressure

Backpressure é quando o sistema para de aceitar mais carga do que consegue processar.

Sem backpressure:

Recebe tudo → fila cresce → memória sobe → timeout geral → queda total

Com backpressure:

Recebe até o limite → rejeita excesso → preserva operações em andamento

Exemplo com Channel limitado:

var channel = Channel.CreateBounded<Comando>(new BoundedChannelOptions(10_000)
{
FullMode = BoundedChannelFullMode.DropWrite,
SingleReader = true,
SingleWriter = false
});

Exemplo de rejeição:

if (!channel.Writer.TryWrite(comando))
{
return Results.Problem(
title: "Sistema em alta carga",
detail: "A requisição foi rejeitada para preservar a estabilidade.",
statusCode: StatusCodes.Status429TooManyRequests);
}

Status comuns:

StatusUso
429 Too Many Requestscliente excedeu limite
503 Service Unavailableserviço temporariamente indisponível
408 Request Timeoutrequisição demorou demais
202 Acceptedaceito para processamento assíncrono

7. Fail Fast

Fail Fast é rejeitar rapidamente quando o sistema sabe que não consegue atender.

Exemplo ruim:

Usuário espera 40s → recebe erro

Exemplo bom:

Sistema detecta saturação → responde em 100ms com 429 ou 503

Isso protege:

  • thread pool
  • conexões de banco
  • memória
  • filas
  • CPU
  • dependências downstream

Exemplo:

app.MapPost("/relatorios", async (
IFeatureManager featureManager,
IRelatorioService service) =>
{
if (!await featureManager.IsEnabledAsync("GeracaoRelatorios"))
{
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}

await service.GerarAsync();

return Results.Accepted();
});

8. Redução de escopo

Nem sempre a funcionalidade precisa ser desligada totalmente.

Ela pode operar em modo reduzido.

Exemplo:

NormalDegradado
extrato de 90 diasextrato de 7 dias
dashboard em tempo realdashboard com cache de 5 min
relatório completorelatório resumido
busca com filtros avançadosbusca simples
payload completopayload reduzido
processamento síncronoprocessamento assíncrono

Exemplo:

var dias = await featureManager.IsEnabledAsync("Extrato90Dias")
? 90
: 7;

var extrato = await extratoService.BuscarAsync(contaId, dias);

9. Processamento assíncrono

Quando uma operação é pesada, o sistema pode aceitar a solicitação e processar depois.

Em vez de:

POST /relatorio → espera 60s → timeout

Melhor:

POST /relatorio → 202 Accepted → processa em background → notifica depois

Exemplo:

app.MapPost("/relatorios", async (
RelatorioRequest request,
IBackgroundQueue queue) =>
{
var requestId = Guid.NewGuid();

await queue.EnqueueAsync(new GerarRelatorioCommand(
RequestId: requestId,
UsuarioId: request.UsuarioId));

return Results.Accepted($"/relatorios/{requestId}", new
{
requestId,
status = "processing"
});
});

Esse padrão reduz pressão em rotas críticas.


10. Priorização de tráfego

Nem toda requisição deve disputar recurso igualmente.

Exemplo:

PrioridadeRequisição
Altapagamento, PIX, login
Médiasaldo, extrato curto
Baixarelatórios, recomendações, analytics

Em alta carga, o sistema deve proteger as rotas de prioridade alta.

Exemplo prático:

/pix/enviar              → prioridade alta
/saldo → prioridade alta
/extrato/7-dias → prioridade média
/extrato/90-dias → prioridade baixa
/relatorios/exportar → prioridade baixa

Em .NET, isso pode ser implementado com:

  • rate limiting por rota
  • filas separadas
  • semáforos por operação
  • feature flags
  • policies diferentes por endpoint
  • bulkheads

11. Bulkhead

Bulkhead isola recursos para evitar que uma parte derrube o sistema inteiro.

Exemplo:

Pool A → pagamentos
Pool B → relatórios
Pool C → notificações

Se relatórios saturarem, pagamentos continuam operando.

Em .NET, pode ser feito com SemaphoreSlim:

public sealed class RelatorioService
{
private static readonly SemaphoreSlim _limiter = new(
initialCount: 5,
maxCount: 5);

public async Task GerarAsync(CancellationToken ct)
{
if (!await _limiter.WaitAsync(TimeSpan.FromMilliseconds(100), ct))
{
throw new TooManyRequestsException("Relatórios em alta carga.");
}

try
{
await ProcessarRelatorioAsync(ct);
}
finally
{
_limiter.Release();
}
}
}

12. Observabilidade para ativar degradação

Graceful degradation sem observabilidade vira chute.

Você precisa medir:

  • latência por endpoint
  • taxa de erro
  • timeout
  • uso de CPU
  • uso de memória
  • tamanho de fila
  • conexões abertas
  • tempo de resposta do banco
  • thread pool starvation
  • saturação de Redis
  • percentis p95, p99

Exemplo de métrica útil:

db_query_duration_p95 > 2s

Ação:

desativar extrato 90 dias
ativar cache fallback
reduzir payload
bloquear relatórios

Como sistemas modernos combinam essas estratégias

Um fluxo realista seria:

1. Banco começa a responder acima do SLA
2. Métricas detectam aumento de latência
3. Circuit breaker começa a abrir para operações caras
4. Feature flags desligam funcionalidades secundárias
5. Cache fallback assume leituras não críticas
6. Backpressure rejeita excesso de carga
7. Operações críticas continuam com prioridade
8. Alertas são enviados para o time

Exemplo de decisão arquitetural

Se o banco estiver lento:
FuncionalidadeDecisão
Loginmanter
PIXmanter
Saldo atualtentar banco, depois cache
Extrato 7 diasmanter se possível
Extrato 90 diasdesligar
Exportação PDFdesligar
Dashboard analíticocache
Recomendaçõesdesligar

Exemplo em .NET com decisão por modo degradado

public sealed class DegradationOptions
{
public bool ModoDegradadoAtivo { get; init; }
public bool PermitirExtrato90Dias { get; init; }
public bool PermitirExportacaoPdf { get; init; }
public bool UsarCacheFallback { get; init; }
}
{
"Degradation": {
"ModoDegradadoAtivo": true,
"PermitirExtrato90Dias": false,
"PermitirExportacaoPdf": false,
"UsarCacheFallback": true
}
}
app.MapGet("/extrato", async (
IOptionsMonitor<DegradationOptions> options,
IExtratoService extratoService,
string contaId) =>
{
var degradation = options.CurrentValue;

var dias = degradation.PermitirExtrato90Dias ? 90 : 7;

var extrato = await extratoService.BuscarAsync(contaId, dias);

return Results.Ok(new
{
modoDegradado = degradation.ModoDegradadoAtivo,
diasConsultados = dias,
dados = extrato
});
});

Boas práticas

PráticaMotivo
classificar funcionalidades por criticidadesaber o que sacrificar primeiro
configurar timeoutsevitar recursos presos
usar cache fallback com cuidadomanter disponibilidade
aplicar backpressureimpedir colapso total
usar feature flagsdesligar partes sem deploy
evitar retries agressivosnão amplificar falhas
usar circuit breakerparar chamadas para dependências quebradas
monitorar p95/p99média esconde degradação
responder rápido com erro controladomelhor que timeout lento
testar modo degradadofeature não testada falha na crise

Anti-patterns

Anti-patternProblema
retry infinitoamplifica a falha
timeout muito altoprende threads e conexões
cache sem TTLentrega dado velho sem controle
tudo síncronoaumenta chance de timeout
uma fila única para tudotarefas ruins afetam tarefas críticas
banco compartilhado para tudogargalo único
sem feature flagexige deploy para reagir
sem observabilidadedegradação vira achismo
responder sempre 500cliente não sabe se deve tentar de novo
deixar usuário esperandopiora UX e consome recurso

Pergunta de entrevista

O que você faz quando o banco fica lento?

Resposta fraca:

Aumento CPU, memória ou faço scale up.

Resposta melhor:

Primeiro eu trato banco lento como cenário esperado, não como exceção. 
Eu configuro timeouts, circuit breaker, cache fallback, backpressure e feature flags.
Funcionalidades críticas continuam com prioridade, enquanto funcionalidades caras ou secundárias são desativadas ou reduzidas.
Também monitoro p95/p99, taxa de erro, pool de conexões e tempo das queries para decidir quando entrar e sair do modo degradado.

Checklist prático

[ ] Tenho lista de funcionalidades críticas?
[ ] Tenho lista de funcionalidades que podem cair?
[ ] Tenho feature flags para desligar o não essencial?
[ ] Tenho timeout em chamadas externas?
[ ] Tenho timeout em queries?
[ ] Tenho retry com limite?
[ ] Tenho circuit breaker?
[ ] Tenho cache fallback onde faz sentido?
[ ] Tenho backpressure?
[ ] Tenho rate limit por rota?
[ ] Tenho filas separadas por criticidade?
[ ] Tenho métricas p95/p99?
[ ] Tenho alerta para modo degradado?
[ ] Tenho teste automatizado ou manual do modo degradado?