Circuit Breaker
O problema no contexto distribuído
Seção intitulada “O problema no contexto distribuído”Quando um serviço downstream fica lento ou indisponível, os chamadores continuam enviando requisições que penduram em timeouts. Cada chamada presa consome uma thread/conexão do pool do chamador. Se o pool esgota, o chamador também fica indisponível — e quem chama o chamador cai também. É a cascata de falhas (resource exhaustion): uma falha localizada se propaga e derruba a árvore de serviços acima.
O disjuntor quebra essa propagação: em vez de esperar timeouts contra uma dependência morta, ele falha rápido, liberando recursos e contendo o incidente.
A solução: máquina de três estados
Seção intitulada “A solução: máquina de três estados”- Closed (fechado): estado normal. As chamadas passam. O breaker conta falhas numa janela (ex.: sliding window de N chamadas ou de tempo). Se a taxa de falha ultrapassa o limiar → transita para open.
- Open (aberto): as chamadas são rejeitadas imediatamente (ex.:
CallNotPermittedException), sem tocar a dependência. Aciona-se o Fallback. Após umwaitDuration→ transita para half-open. - Half-open (semiaberto): permite um número limitado de chamadas de sondagem. Se tiverem sucesso (dependência recuperada) → volta a closed. Se falharem → volta a open e reinicia a espera.
stateDiagram-v2
[*] --> Closed
Closed --> Open: taxa de falha > limiar
Open --> HalfOpen: após waitDuration
HalfOpen --> Closed: sondagens com sucesso
HalfOpen --> Open: sondagem falhou
note right of Open
rejeita chamadas (fail-fast)
aciona Fallback
end note
note right of HalfOpen
permite N chamadas de teste
end note
Parâmetros de configuração
Seção intitulada “Parâmetros de configuração”- failureRateThreshold: % de falhas na janela que abre o circuito (ex.: 50%).
- slidingWindow (count-based ou time-based): quantas chamadas/segundos são avaliados.
- minimumNumberOfCalls: mínimo de amostras antes de calcular a taxa (evita abrir por 1 falha em 2 chamadas).
- waitDurationInOpenState: tempo em open antes de sondar.
- permittedNumberOfCallsInHalfOpenState: quantas sondagens em half-open.
- slowCallRateThreshold / slowCallDurationThreshold: trata chamadas lentas (não só erros) como falha — essencial, pois lentidão é o que esgota pools.
Variações
Seção intitulada “Variações”- Contagem por erros vs por lentidão: além de exceções, contar chamadas que excedem um limite de duração como falha.
- Bulkhead (compartimentação): isola pools de recursos por dependência para que uma não afete as outras — complementar ao breaker.
- Rate limiter: relacionado, mas protege por taxa, não por falha.
Trade-offs
Seção intitulada “Trade-offs”- Prós: contém cascata, libera recursos, dá tempo de recuperação à dependência, melhora estabilidade global.
- Contras: pode rejeitar chamadas que teriam sucesso (falsos positivos se mal calibrado); exige tuning cuidadoso dos limiares; adiciona um estado a observar e monitorar.
Quando NÃO usar
Seção intitulada “Quando NÃO usar”- Em chamadas locais/in-process que não fazem I/O de rede — não há cascata a conter.
- Para dependências sem alternativa e sem custo de cascata onde falhar rápido não ajuda (raro).
- Quando a dependência tem falhas legítimas frequentes que não indicam indisponibilidade — o breaker abriria à toa; ajuste o limiar em vez de aplicar cegamente.
Exemplo Java/Spring com Resilience4j
Seção intitulada “Exemplo Java/Spring com Resilience4j”Circuit breaker + Retry + Fallback na mesma chamada. A ordem de composição importa: retry por fora do breaker, fallback cobrindo tudo.
@Configurationpublic class ResilienceConfig {
@Bean public CircuitBreakerRegistry circuitBreakerRegistry() { var config = CircuitBreakerConfig.custom() .slidingWindowType(SlidingWindowType.COUNT_BASED) .slidingWindowSize(20) .minimumNumberOfCalls(10) .failureRateThreshold(50f) .slowCallRateThreshold(80f) .slowCallDurationThreshold(Duration.ofSeconds(2)) .waitDurationInOpenState(Duration.ofSeconds(10)) .permittedNumberOfCallsInHalfOpenState(3) .build(); return CircuitBreakerRegistry.of(config); }}
@Service@RequiredArgsConstructorpublic class InventoryGateway {
private final RestClient restClient;
@Retry(name = "inventory", fallbackMethod = "unknownStock") @CircuitBreaker(name = "inventory") public StockLevel checkStock(String sku) { return restClient.get() .uri("/stock/{sku}", sku) .retrieve() .body(StockLevel.class); }
private StockLevel unknownStock(String sku, Throwable ex) { if (ex instanceof CallNotPermittedException) { log.warn("Circuito ABERTO para inventory, sku={}", sku); } return StockLevel.unknown(sku); }}Com Spring AOP/Resilience4j, a ordem default é @Retry por fora de @CircuitBreaker: cada tentativa do retry conta como uma chamada na janela do breaker. Por isso o fallbackMethod fica no @Retry, o aspecto mais externo — cobre tanto CallNotPermittedException (circuito aberto) quanto o esgotamento das tentativas. Se ficasse no @CircuitBreaker (interno), o fallback devolveria um valor normal na primeira falha e o retry externo nunca dispararia.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Limiar mal calibrado:
minimumNumberOfCallsbaixo abre o circuito por ruído estatístico; threshold alto demais nunca protege. - Ignorar chamadas lentas: contar só exceções e não lentidão — mas é a lentidão que esgota os pools. Configure
slowCallRateThreshold. - Um breaker global para várias dependências: uma dependência ruim abre o circuito das saudáveis. Use um breaker por dependência (idealmente com bulkhead).
- Sem Fallback: circuito aberto lançando exceção crua ao usuário — perde-se metade do valor do padrão.
waitDurationInOpenStatecurto demais: volta a sondar antes de a dependência se recuperar, oscilando entre open e half-open.- Retry agressivo sem breaker: os retries martelam a dependência morta; o breaker precisa cortá-los.
Relações
Seção intitulada “Relações”- Combina com Retry (interrompe tentativas contra dependência morta) e Fallback (resposta enquanto aberto).
- Protege consultas de API Composition e chamadas remotas em geral.
- Contexto: Microservices, Sistemas Distribuídos, APIs - REST, GraphQL e gRPC.
- Índice: Microservices Patterns.
Perguntas de revisão
Seção intitulada “Perguntas de revisão”- Descreva os três estados do circuit breaker e suas transições.
Resposta
Closed: chamadas passam e falhas são contadas; se a taxa de falha excede o limiar, vai para open. Open: chamadas são rejeitadas imediatamente (fail-fast) sem tocar a dependência; após waitDuration, vai para half-open. Half-open: permite um número limitado de sondagens; se têm sucesso, volta a closed; se falham, volta a open e reinicia a espera.
- Que problema o circuit breaker previne e como?
Resposta
Previne a cascata de falhas por esgotamento de recursos: chamadas presas em timeout contra uma dependência lenta/morta consomem threads e conexões do chamador até esgotá-las, derrubando-o e propagando a falha para cima. Ao abrir o circuito e falhar rápido, o breaker libera esses recursos e contém a propagação.
- Por que contar chamadas lentas (não só erros) é importante?
Resposta
Porque a lentidão, mais que os erros, é o que esgota os pools: chamadas que demoram (mas não falham) seguram threads/conexões. Um breaker que só conta exceções não abriria diante de uma dependência degradada porém “respondendo”, e a cascata aconteceria mesmo assim. Por isso configura-se slowCallRateThreshold e slowCallDurationThreshold.
- Por que usar um circuit breaker por dependência em vez de um global?
Resposta
Porque um breaker global mistura os sinais de todas as dependências: falhas de um serviço ruim abririam o circuito, bloqueando também as chamadas a serviços saudáveis. Um breaker por dependência (idealmente com bulkhead isolando os pools) garante que a proteção seja localizada e não penalize dependências que estão funcionando.