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ível | Tipo | Exemplo | Deve continuar? |
|---|---|---|---|
| Crítico | Core business | PIX, pagamento, login, saldo | Sim |
| Importante | Apoio ao core | extrato recente, notificações importantes | Preferencialmente |
| Secundário | Experiência | recomendações, dashboard, histórico longo | Pode cair |
| Pesado | Custo alto | relatórios, exportação PDF, analytics | Deve 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:
| Dado | Pode usar cache fallback? |
|---|---|
| saldo exibido em tela | Sim, com aviso |
| lista de produtos | Sim |
| configurações públicas | Sim |
| extrato recente | Depende |
| confirmação de pagamento | Não recomendado |
| débito efetivo em conta | Nã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ário | Retry? |
|---|---|
| erro de rede transitório | Sim |
| HTTP 500 eventual | Sim, com limite |
| timeout por sobrecarga | Com cuidado |
| erro de validação | Não |
| saldo insuficiente | Não |
| dado inválido | Nã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:
| Status | Uso |
|---|---|
429 Too Many Requests | cliente excedeu limite |
503 Service Unavailable | serviço temporariamente indisponível |
408 Request Timeout | requisição demorou demais |
202 Accepted | aceito 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:
| Normal | Degradado |
|---|---|
| extrato de 90 dias | extrato de 7 dias |
| dashboard em tempo real | dashboard com cache de 5 min |
| relatório completo | relatório resumido |
| busca com filtros avançados | busca simples |
| payload completo | payload reduzido |
| processamento síncrono | processamento 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:
| Prioridade | Requisição |
|---|---|
| Alta | pagamento, PIX, login |
| Média | saldo, extrato curto |
| Baixa | relató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:
| Funcionalidade | Decisão |
|---|---|
| Login | manter |
| PIX | manter |
| Saldo atual | tentar banco, depois cache |
| Extrato 7 dias | manter se possível |
| Extrato 90 dias | desligar |
| Exportação PDF | desligar |
| Dashboard analítico | cache |
| Recomendações | desligar |
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ática | Motivo |
|---|---|
| classificar funcionalidades por criticidade | saber o que sacrificar primeiro |
| configurar timeouts | evitar recursos presos |
| usar cache fallback com cuidado | manter disponibilidade |
| aplicar backpressure | impedir colapso total |
| usar feature flags | desligar partes sem deploy |
| evitar retries agressivos | não amplificar falhas |
| usar circuit breaker | parar chamadas para dependências quebradas |
| monitorar p95/p99 | média esconde degradação |
| responder rápido com erro controlado | melhor que timeout lento |
| testar modo degradado | feature não testada falha na crise |
Anti-patterns
| Anti-pattern | Problema |
|---|---|
| retry infinito | amplifica a falha |
| timeout muito alto | prende threads e conexões |
| cache sem TTL | entrega dado velho sem controle |
| tudo síncrono | aumenta chance de timeout |
| uma fila única para tudo | tarefas ruins afetam tarefas críticas |
| banco compartilhado para tudo | gargalo único |
| sem feature flag | exige deploy para reagir |
| sem observabilidade | degradação vira achismo |
responder sempre 500 | cliente não sabe se deve tentar de novo |
| deixar usuário esperando | piora 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?