Degradação graciosa: o sistema que não cai junto com a dependência
Timeout, retry com jitter, circuit breaker, bulkhead e fallback: os padrões de resiliência e a decisão de arquitetura de "dizer não" a tempo.
Um sistema distribuído de verdade não é uma questão de se uma dependência vai falhar, mas de quando. O provedor de pagamento vai degradar, o serviço de recomendação vai ficar lento, o banco vai engasgar num pico. A pergunta que separa arquitetura madura de arquitetura ingênua é simples: quando a dependência cair, o seu sistema cai junto ou degrada de forma controlada?
Degradação graciosa é a arte de perder função sem perder o serviço. O carrinho continua funcionando mesmo com o serviço de recomendação fora; a listagem carrega mesmo com o cache indisponível; o pagamento aceita o pedido e concilia depois mesmo com um subsistema lento. O sistema faz menos, mas continua de pé.
Os padrões que sustentam isso
Nenhum desses padrões é novo, e é exatamente por isso que impressiona quantos sistemas em produção não têm nenhum deles. Vamos aos que importam.
1. Timeout — o mais básico e o mais esquecido. Uma chamada sem timeout é uma bomba-relógio. Se a dependência trava, a sua thread trava esperando, e sob carga isso esgota o pool de conexões inteiro. Um serviço morre muito mais frequentemente por esperar demais do que por falhar rápido. Todo cliente de rede — HTTP, banco, fila, cache — precisa de timeout explícito. Sem exceção.
2. Retry com jitter. Retry ajuda em falha transitória. Retry mal feito derruba o que já estava se recuperando. Se todos os clientes tentam de novo ao mesmo tempo, você cria um thundering herd que empurra a dependência de volta para o chão no exato momento em que ela tentava levantar. A solução é backoff exponencial com jitter — aleatoriedade que espalha as tentativas no tempo.
import random, time
def retry_com_jitter(chamada, tentativas=3, base=0.1, teto=2.0):
for i in range(tentativas):
try:
return chamada()
except FalhaTransitoria:
if i == tentativas - 1:
raise
# backoff exponencial + jitter: espalha as retentativas no tempo
espera = min(teto, base * (2 ** i))
time.sleep(random.uniform(0, espera))3. Circuit breaker — saber a hora de parar de tentar. Quando uma dependência está claramente fora, insistir só piora: você gasta recurso, prende threads e atrasa a resposta de erro que o usuário vai receber de qualquer jeito. O circuit breaker monitora a taxa de falha e, ao cruzar um limiar, "abre" — passa a falhar na hora, sem nem tentar a chamada. Depois de um tempo, deixa passar uma chamada de teste; se voltar, "fecha" de novo. É o padrão que impede a falha de uma dependência de virar exaustão de recursos no seu lado.
4. Bulkhead — compartimentar para não afundar o navio inteiro. O nome vem das divisórias estanques de um navio: um compartimento inunda, os outros seguem secos. Em software, significa isolar recursos por dependência. Se todas as chamadas — para o serviço lento e para os rápidos — dividem o mesmo pool de threads, a dependência lenta consome o pool inteiro e derruba tudo por tabela. Pools separados por dependência contêm o dano.
5. Fallback — ter um plano B honesto. Quando a chamada falha (ou o breaker está aberto), o que você devolve? Um valor em cache, um default sensato, uma resposta parcial, uma mensagem clara de "indisponível no momento". O fallback é o que transforma "erro 500 na cara do usuário" em "funciona, só que com menos".
A decisão de arquitetura de verdade: dizer não a tempo
Aqui está a parte que os frameworks não resolvem por você. Timeout, breaker e bulkhead são mecanismos. A decisão difícil é de design: quais funcionalidades são essenciais e quais são descartáveis sob pressão?
Isso é uma conversa de produto e engenharia junto, não um detalhe de implementação. No checkout de um e-commerce, autorizar o pagamento é essencial; calcular pontos de fidelidade em tempo real é descartável — dá para conciliar depois. Numa fintech, registrar a transação é sagrado; enriquecer com dados analíticos pode esperar. Se você não decidiu isso antes do incidente, vai decidir no meio dele, sob estresse, provavelmente errado.
Degradar bem é dizer "não" para o secundário a tempo de proteger o primário. Um sistema que trata tudo como igualmente crítico não degrada — ele só cai.
O caso real, sem nome
Numa fintech brasileira em escala (grupo global de pagamentos), com um volume na casa de dezenas de milhões de transações por dia, a diferença entre um incidente de dependência que virava indisponibilidade total e um que passava despercebido pelos clientes estava justamente nesses padrões. Um subsistema de enriquecimento de dados degradava periodicamente. Na versão ingênua, ele prendia threads e a latência disso vazava para o fluxo de pagamento inteiro. Depois de isolar com bulkhead, colocar timeout agressivo e um fallback que seguia o pagamento e enriquecia de forma assíncrona, a mesma degradação virou não-evento: o pagamento passava, o enriquecimento acontecia minutos depois, o cliente não via nada.
O padrão não eliminou a falha da dependência. Ele desacoplou o destino do sistema do destino da dependência. Isso é o objetivo inteiro.
Onde isso costuma dar errado
Alguns erros recorrentes que vale conhecer de antemão:
- Timeout longo demais. Um timeout de 30s "por segurança" não protege nada — ele só demora 30s para falhar. Timeout tem que ser proporcional ao SLA da operação, não ao seu medo de errar.
- Retry empilhado em camadas. Se cada camada da stack tem retry próprio, três tentativas viram vinte e sete. Retry deve viver em uma camada, com orçamento total explícito.
- Fallback que mente. Devolver dado velho de cache sem sinalizar que está velho pode ser pior que falhar. Fallback honesto deixa claro — para o sistema e às vezes para o usuário — que aquilo é uma resposta degradada.
Resiliência não é uma biblioteca que você instala. É um conjunto de decisões de arquitetura sobre o que é essencial, o que é descartável, e quão rápido você está disposto a dizer "não". A boa notícia: os mecanismos são bem conhecidos. A parte difícil, e a que exige senioridade, é a decisão de negócio por trás deles.