MÓDULO 08 · CONCEITO 04 DE 14

Graceful Degradation em profundidade

Modo degradado como contrato explícito — sistema que falha parcialmente é melhor que sistema que falha completamente

Tempo de leitura ~22 min Pré-requisito 03 · Bulkhead e isolation patterns Próximo 05 · Feature flags & progressive delivery

Todo sistema complexo falha. A questão não é se, mas quando e como. Graceful degradation é a propriedade de um sistema de continuar entregando valor parcial sob condições de falha — em vez de falhar completamente. Um servidor web que continua respondendo requisições básicas mesmo quando o sistema de recomendação está fora não está "funcionando normalmente", mas está funcionando. Um app de pagamento que processa transações sem exibir o histórico de compras está degradado, mas operando. Um mecanismo de busca que retorna resultados em cache levemente desatualizados em vez de nada é degradado, mas útil.

A intuição de graceful degradation não é nova — o design de aviões há décadas usa o conceito de "fail-safe": quando um componente falha, o sistema cai para um modo de operação mais simples mas seguro, não para modo de catástrofe. Pat Helland e Dave Campbell, em "Building on Quicksand" (CIDR, 2009), articularam isso para sistemas distribuídos: "o sistema deve funcionar continuamente, mesmo quando partes falham". O que mudou é a capacidade de implementar isso de forma programática e sistemática — não como gambiarra de emergência, mas como propriedade projetada.

O conceito complementa diretamente o bulkhead (conceito anterior): o bulkhead isola o impacto da falha em um compartimento; a degradação graciosa define o que acontece no comportamento do sistema depois que o bulkhead ativou. Sem ambos, um bulkhead que rejeita chamadas ao serviço de recomendações simplesmente faz a página quebrar — que é melhor que o timeout travar o checkout, mas ainda não é ótimo.

A hierarquia de criticidade de features

O primeiro passo para implementar degradação graciosa é classificar explicitamente as features e dependências do sistema por criticidade. Sem essa classificação, o sistema não sabe o que pode desligar quando está sob pressão. A classificação típica usa três tiers:

Tier 1 — Crítico: sem isso, o sistema não entrega valor algum. Para um e-commerce: listar produtos, adicionar ao carrinho, processar pagamento, confirmar pedido. Se qualquer uma dessas falhar, o usuário não consegue completar a jornada principal. Essas features nunca são degradadas — elas devem ter redundância, fallback de última instância, e SLO mais agressivo.

Tier 2 — Importante, mas não bloqueante: features que enriquecem a experiência mas cuja falha não impede o core. Para o mesmo e-commerce: recomendações personalizadas, histórico de pedidos, notificações por email, avaliações de produtos. Se o serviço de recomendação cair, o usuário ainda consegue comprar — ele só vai ver uma seção vazia onde apareceriam as recomendações, ou um placeholder.

Tier 3 — Periférico: features nice-to-have cuja falha dificilmente o usuário percebe no fluxo principal. Analytics em tempo real, sync com CRM externo, geração de relatórios gerenciais, logs de auditoria não-críticos. Quando o sistema está sob pressão, estas são as primeiras a serem desligadas.

heurística do sênior

A classificação de criticidade não é técnica — é de negócio. Engenharia pode sugerir, mas product ownership deve validar. "Avaliações de produtos são Tier 2 ou Tier 3?" é uma pergunta que um PM responde melhor que um engenheiro. O erro é fazer essa classificação sozinho no código, sem alinhamento com o negócio.

Tipos de fallback

Dado que uma dependência falhou ou foi isolada pelo bulkhead, o sistema precisa de um comportamento alternativo. Os tipos de fallback formam um espectro do mais dinâmico ao mais estático:

Fallback para cache: servir dados cacheados, potencialmente desatualizados, em vez de nada. O serviço de catálogo de produtos falhou? Servir a última versão do catálogo salva em cache local ou Redis. O dado pode estar desatualizado por horas, mas ainda é infinitamente melhor que uma página de erro. Importante: o cache deve ter TTL longo o suficiente para cobrir o tempo esperado de recuperação da dependência.

Fallback para versão simplificada: executar uma versão mais simples da mesma funcionalidade que não depende da dependência falhando. O motor de busca personalizado falhou? Retornar resultados de busca por relevância estática (sem personalização). O serviço de precificação dinâmica falhou? Retornar preço base do catálogo.

Fallback para dado default: retornar um valor razoável quando nenhum dado real está disponível. Recomendações personalizadas falharam? Retornar lista de produtos mais vendidos (pré-computada, sempre disponível). Foto de perfil falhando? Retornar avatar gerado por inicial do nome.

Fallback para operação offline/assíncrona: aceitar a operação e processá-la quando a dependência se recuperar. Envio de email falhou? Enfileirar na outbox e tentar novamente. Sistema de analytics fora? Acumular eventos em buffer local e enviar em batch depois.

Degradação visível ao usuário: quando nenhum fallback silencioso é possível, informar ao usuário de forma clara mas não alarmante. "Recomendações temporariamente indisponíveis" é melhor que spinner infinito ou mensagem de erro técnica.

armadilha clássica

Fallback que silencia o problema é perigoso. Um sistema que serve cache stale por 48 horas sem alertar ninguém está escondendo uma falha. Toda degradação deve ser observada: log, métrica, alerta. A degradação graciosamente não significa degradação silenciosamente.

Modelo de dependências e grafo de degradação

Para implementar degradação sistematicamente, é útil modelar as dependências do sistema como grafo, anotado com criticidade e fallback disponível. Isso permite raciocinar sobre o que acontece com o sistema quando combinações de dependências falham.

Dependências da API de checkout:
  banco_principal (Tier 1) → sem fallback → falha total
    ├── banco_replica (Tier 1) ← fallback para principal
  serviço_estoque (Tier 1) → cache_estoque (fallback 30min)
  serviço_pagamento (Tier 1) → sem fallback → rejeitar pedido
  serviço_fraude (Tier 2) → aprovar com flag de review manual
  serviço_email (Tier 2) → outbox + retry assíncrono
  serviço_recomendações (Tier 3) → lista mais vendidos (estática)
  analytics_pipeline (Tier 3) → buffer local + flush posterior

Cenário de falha: serviço_fraude fora por 30min
→ checkout continua funcionando (Tier 1 intacto)
→ pedidos são aprovados com flag "fraud_review_pending"
→ team de fraude processa manualmente ao recuperar

Cenário de falha: serviço_pagamento fora por 5min
→ checkout rejeita pedidos com erro 503 amigável
→ usuário vê: "Pagamento temporariamente indisponível.
   Seu carrinho foi salvo."
→ budget de erro é consumido em proporção ao volume

Feature flags como mecanismo de degradação

Feature flags (detalhados no próximo conceito) são o mecanismo natural para implementar degradação graciosa de forma controlável. Em vez de implementar degradação apenas como reação automática a falhas, feature flags permitem degradar proativamente antes que a falha aconteça.

Cenários onde degradação proativa via feature flag faz sentido:

A combinação de degradação automática (reativa à falha) e degradação proativa (via flag controlado por humano) cria um sistema que pode operar em múltiplos modos deliberados, não apenas no modo "tudo ligado ou tudo caiu".

Degradação e experiência do usuário

Um aspecto frequentemente negligenciado da degradação graciosa é o design da experiência degradada. Não basta que o sistema tecnicamente continue funcionando em modo degradado — o usuário precisa entender o que está acontecendo e o que pode fazer.

Três princípios para UX de degradação:

Visibilidade proporcional ao impacto: degradações silenciosas (fallback transparente para o usuário) não precisam de comunicação. Degradações que mudam o que o usuário pode fazer devem ser comunicadas claramente, mas sem linguagem técnica ou alarmismo. "Recomendações temporariamente indisponíveis" é bom. "Serviço de recomendações retornou HTTP 503" é péssimo.

Preservar a ação principal: quando possível, o fluxo que o usuário está tentando completar deve sobreviver à degradação. Um email que não envia não deve bloquear a confirmação do pedido. Um histórico que carrega vazio não deve bloquear a página de perfil.

Indicar recuperação quando possível: "Este recurso voltará em breve" ou "Tente novamente em alguns minutos" define expectativa e reduz frustração. Quando o sistema sabe que a degradação é temporária (manutenção planejada, burst de tráfego), deve comunicar isso.

C# — Degradação com fallback em cadeia
public class ProductRecommendationService
{
    private readonly IPersonalizedRecommendations _personalized;
    private readonly IPopularProductsCache _popular;
    private readonly ILogger _logger;

    public async Task<RecommendationResult> GetAsync(
        string userId, CancellationToken ct)
    {
        // Tier 1: personalizado (dependência externa)
        try
        {
            var items = await _personalized.GetForUserAsync(userId, ct)
                .WaitAsync(TimeSpan.FromMilliseconds(300), ct);
            return RecommendationResult.Personalized(items);
        }
        catch (Exception ex) when (ex is TimeoutException
                                    or OperationCanceledException
                                    or HttpRequestException)
        {
            _logger.LogWarning(ex,
                "Recomendações personalizadas indisponíveis — degradando");
        }

        // Tier 2: mais vendidos (cache local, nunca falha)
        var popular = await _popular.GetTopAsync(count: 10, ct);
        return RecommendationResult.Degraded(popular,
            reason: "personalization_unavailable");
    }
}

public record RecommendationResult(
    IReadOnlyList<Product> Items,
    bool IsDegraded,
    string? DegradationReason)
{
    public static RecommendationResult Personalized(IReadOnlyList<Product> items)
        => new(items, false, null);

    public static RecommendationResult Degraded(
        IReadOnlyList<Product> items, string reason)
        => new(items, true, reason);
}

O resultado carrega o estado de degradação explicitamente — o controller pode usar IsDegraded para enviar header de telemetria ou ajustar a resposta JSON para o cliente.

Python — Fallback em cadeia com anotação de degradação
from dataclasses import dataclass, field
from enum import Enum


class DegradationLevel(Enum):
    FULL = "full"
    PARTIAL = "partial"
    MINIMAL = "minimal"


@dataclass
class RecommendationResult:
    items: list[dict]
    level: DegradationLevel
    reason: str | None = None

    @property
    def is_degraded(self) -> bool:
        return self.level != DegradationLevel.FULL


class ProductRecommendationService:
    def __init__(self, personalized_client, popular_cache, logger):
        self._personalized = personalized_client
        self._popular_cache = popular_cache
        self._log = logger

    async def get(self, user_id: str) -> RecommendationResult:
        # Tier 1: personalizado — timeout curto
        try:
            items = await asyncio.wait_for(
                self._personalized.get_for_user(user_id),
                timeout=0.3
            )
            return RecommendationResult(items, DegradationLevel.FULL)
        except (asyncio.TimeoutError, ClientError) as e:
            self._log.warning("Recomendações personalizadas falhou", exc_info=e)

        # Tier 2: cache de mais vendidos
        try:
            popular = await self._popular_cache.get_top(count=10)
            return RecommendationResult(
                popular,
                DegradationLevel.PARTIAL,
                reason="personalization_unavailable"
            )
        except Exception as e:
            self._log.error("Cache de popular também falhou", exc_info=e)

        # Tier 3: lista vazia com indicação ao usuário
        return RecommendationResult(
            [],
            DegradationLevel.MINIMAL,
            reason="recommendations_unavailable"
        )

O nível de degradação (DegradationLevel) permite ao frontend tomar decisões: PARTIAL mostra o bloco com disclaimer; MINIMAL esconde o bloco completamente.

Go — Degradação com contexto e fallback
type DegradationLevel int

const (
    FullService DegradationLevel = iota
    PartialService
    MinimalService
)

type RecommendationResult struct {
    Items    []Product
    Level    DegradationLevel
    Reason   string
}

func (r RecommendationResult) IsDegraded() bool {
    return r.Level != FullService
}

type RecommendationService struct {
    personalized PersonalizedClient
    popular      PopularCache
    log          *slog.Logger
}

func (s *RecommendationService) Get(
    ctx context.Context, userID string,
) RecommendationResult {
    // Tier 1: personalizado com timeout
    tctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
    defer cancel()

    items, err := s.personalized.GetForUser(tctx, userID)
    if err == nil {
        return RecommendationResult{Items: items, Level: FullService}
    }
    s.log.Warn("recomendações personalizadas falharam",
        "user", userID, "err", err)

    // Tier 2: mais vendidos do cache
    popular, err := s.popular.GetTop(ctx, 10)
    if err == nil {
        return RecommendationResult{
            Items: popular, Level: PartialService,
            Reason: "personalization_unavailable",
        }
    }
    s.log.Error("cache de popular também falhou", "err", err)

    // Tier 3: vazio
    return RecommendationResult{Level: MinimalService,
        Reason: "recommendations_unavailable"}
}

Em Go, a propagação de contexto garante que timeouts se propaguem corretamente pela cadeia de fallback. O log estruturado (slog) por nível permite correlacionar eventos de degradação com traces OpenTelemetry.

Degradação e testes

Degradação graciosa precisa ser testada tanto quanto o caminho feliz — talvez mais, porque é menos exercitada em operação normal. As categorias de teste:

Teste unitário do fallback: quando a dependência retorna erro, o sistema usa o fallback correto? Isso é testável com doubles: stub que sempre falha, verificação de que o resultado é do fallback.

Teste de integração de modo degradado: com a dependência real em modo de falha simulada (container com fault injection, WireMock retornando 503), o sistema completa a operação no modo degradado? O resultado é correto?

Teste de observabilidade: quando o sistema degrada, os logs, métricas e traces registram o evento de degradação? Um incidente que passou despercebido porque as métricas não cobriam o modo degradado é um bug de observabilidade.

Teste de recuperação: quando a dependência se recupera, o sistema volta ao modo normal automaticamente? Sem restart manual? O cache stale é invalidado?

Degradação e SLO

Uma pergunta importante: degradação graciosa conta como falha no SLO? Depende de como o SLO foi definido. Se o SLI é "fração de requisições que retornam HTTP 200 com resposta não-degradada", então uma resposta degradada (200 mas com recomendações de fallback) conta como falha. Se o SLI é "fração de requisições que retornam HTTP 200 (qualquer resposta bem-sucedida)", a degradação não conta.

A recomendação é ter SLIs de qualidade além de SLIs de disponibilidade para features críticas. Um SLO de "99% das requisições de checkout retornam confirmação de pedido processado" é mais fiel à experiência do usuário que "99.9% retornam HTTP 200" — o 200 pode ser de um pedido que ficou na fila porque o sistema de pagamento degradou.

Como praticar

  1. Mapear e classificar dependências em tiers. Para um serviço que você conhece bem, liste todas as dependências e classifique em Tier 1/2/3 com o time de produto. Para cada dependência Tier 2 ou 3, defina qual seria o fallback se ela ficasse indisponível por 30 minutos. Documente no README do serviço — não em código obscuro.
  2. Implementar e testar um fallback. Escolha uma dependência Tier 2 sem fallback hoje. Implemente um fallback (cache, versão simplificada, dado default). Escreva um teste que verifica: (a) a dependência falhando ativa o fallback, (b) o fallback retorna um resultado aceitável, (c) o evento de degradação é registrado em métrica. Execute o teste em CI.
  3. Analisar um incidente passado com a perspectiva de degradação. Escolha um incidente recente onde o sistema falhou completamente para alguns usuários. Analise: haveria sido possível degradar graciosamente em vez de falhar? Qual dependência teria precisado de fallback? Qual seria o custo de implementar esse fallback? Isso âncora a discussão em impacto real de negócio.

Referências para aprofundar

  1. livro Release It! — Michael Nygard (Pragmatic Programmers, 2ª ed. 2018). Capítulo sobre stability patterns inclui "Fail Fast", "Handshaking", e "Shed Load" — complementos naturais à degradação graciosa.
  2. artigo Graceful Degradation Isn't Just for Hardware — Kent Beck (substack, 2022). Reflexão sobre o princípio de degradação graciosa além de sistemas distribuídos — útil para calibrar a perspectiva de design.
  3. artigo Patterns for Resilient Architecture — Graceful Degradation — AWS Well-Architected. docs.aws.amazon.com/wellarchitected/latest/reliability-pillar. Framework Well-Architected trata degradação como requisito de confiabilidade.
  4. artigo How Amazon Achieves Graceful Degradation at Scale — Amazon Tech Blog. Descrição real de como o checkout da Amazon opera em modo degradado — múltiplos fallbacks para cada dependência, com exemplos concretos.
  5. paper Building on Quicksand — Helland & Campbell (CIDR, 2009). O paper que articulou o princípio de que sistemas distribuídos devem funcionar continuamente mesmo com falhas — base teórica para degradação graciosa.
  6. livro Designing Distributed Systems — Brendan Burns (O'Reilly, 2018). Capítulo sobre padrões de resiliência inclui sidecar e ambassador patterns que podem implementar degradação transparentemente.
  7. artigo Graceful Degradation in User Interfaces — Smashing Magazine, 2015. Perspectiva de UX — como comunicar modos degradados ao usuário sem alarmar. Complemento ao lado técnico do conceito.
  8. vídeo Failing Gracefully — Patterns for Resilient Systems — SREcon North America, 2020. YouTube. 35 minutos sobre implementação prática de degradação em microserviços, com casos reais de Netflix e Google.
  9. artigo Feature Tiers and Graceful Degradation — Shopify Engineering, 2021. Como a Shopify classifica features em tiers e implementa degradação programática para o Black Friday.
  10. docs Resilience4j — TimeLimiter and Fallback — resilience4j.github.io. Documentação do padrão de fallback em Resilience4j — útil para Java/Kotlin, mas os conceitos são portáveis.
  11. artigo Stale-While-Revalidate and Graceful Degradation — Fastly Blog. Como o padrão stale-while-revalidate de HTTP cache implementa degradação graciosa na camada de CDN.
  12. livro Chaos Engineering — Casey Rosenthal et al. (O'Reilly, 2020). Capítulo sobre "System Characteristics" inclui graceful degradation como atributo a testar em experimentos de chaos.