Menu

Circuit Breaker: Proteção Contra Falhas em Cascata

Como implementar Circuit Breaker, Bulkhead e Rate Limiting para proteger sistemas distribuídos contra falhas em cascata

Circuit Breaker: Proteção Contra Falhas em Cascata

Para quem está começando: explicação simples

O Problema: O “Efeito Dominó” em Sistemas Distribuídos

Imagine um banco com vários sistemas interconectados:

Cenário sem proteção:

  • Sistema PIX está sobrecarregado (Black Friday)
  • Sistema TED depende de validações do PIX
  • Sistema Cartões também consulta os mesmos serviços
  • Resultado: PIX falha → TED falha → Cartões falham → TUDO PARA! 💥

É como um curto-circuito elétrico que queima toda a casa.

A Solução: Circuit Breaker (Disjuntor Digital)

Sistema Bancário com Circuit Breaker:

Estado NORMAL (CLOSED):

  • Serviços funcionando: todas as operações processam normalmente
  • Como um disjuntor “ligado” - energia flui livremente

Estado PROTEÇÃO (OPEN):

  • Serviço falhando muito: Circuit Breaker “desarma”
  • Sistema usa fallbacks (cache, dados locais, valores padrão)
  • Como um disjuntor “desligado” - protege o resto do sistema

Estado TESTE (HALF-OPEN):

  • Após um tempo, testa se serviço voltou
  • Se funcionar: volta ao normal
  • Se falhar: volta à proteção

Analogia da Ponte com Pedágio

Ponte Normal:

  • Carros passam livremente pagando pedágio
  • Sistema de cobrança funcionando bem

Ponte com Problemas:

  • Sistema de cobrança falha
  • SEM Circuit Breaker: Trânsito para, carros acumulam, caos total
  • COM Circuit Breaker: Libera passagem gratuita temporariamente, trânsito flui

Por que isso é crucial no banco?

Cenários reais:

  • PIX instável: Não pode derrubar TEDs e cartões
  • SERASA fora do ar: Não pode parar aprovação de empréstimos
  • BACEN lento: Não pode travar todas as operações
  • Antifraude sobrecarregado: Não pode bloquear todos os pagamentos

Benefícios:

  • Isolamento: Falha em um serviço não afeta outros
  • Graceful degradation: Sistema funciona mesmo com limitações
  • Recuperação automática: Volta ao normal quando possível
  • Experiência do usuário: Cliente não vê erro “tudo quebrado”

Conceitos técnicos

Estados do Circuit Breaker

Um Circuit Breaker possui 3 estados fundamentais:

CLOSED (Fechado): Estado normal - requisições passam livremente OPEN (Aberto): Estado de proteção - requisições são rejeitadas imediatamente HALF_OPEN (Meio-Aberto): Estado de teste - permite algumas requisições para verificar recuperação

Sliding Window Patterns

Count-based Window: Considera últimas N requisições Time-based Window: Considera requisições em janela de tempo T

Métricas de Decisão

Failure Rate: Percentual de falhas Slow Call Rate: Percentual de chamadas lentas Response Time Threshold: Limite de tempo de resposta

Arquitetura: Sistema Bancário Resiliente

flowchart TB
    subgraph "Sistema Bancário Resiliente"
        CLIENT[Cliente App]
        
        subgraph "Camada de Proteção"
            LB[Load Balancer]
            RL[Rate Limiter]
            CB_GATEWAY[Circuit Breaker Gateway]
        end
        
        subgraph "💳 Serviços Core"
            PIX[Serviço PIX]
            TED[Serviço TED]
            CARTAO[Serviço Cartões]
            CONTA[Serviço Contas]
        end
        
        subgraph "🔌 Serviços Externos"
            BACEN[BACEN API]
            SERASA[SERASA API]
            OUTROS_BANCOS[Outros Bancos]
            ANTIFRAUDE[Antifraude API]
        end
        
        subgraph "Bulkheads (Isolamento)"
            POOL_PIX[Thread Pool PIX]
            POOL_TED[Thread Pool TED]
            POOL_CONSULTAS[Thread Pool Consultas]
        end
        
        subgraph "Fallbacks"
            CACHE[Cache Local]
            DEFAULT_SCORES[Scores Padrão]
            OFFLINE_VALIDATION[Validação Offline]
        end
    end
    
    CLIENT --> LB
    LB --> RL
    RL --> CB_GATEWAY
    
    CB_GATEWAY --> PIX
    CB_GATEWAY --> TED
    CB_GATEWAY --> CARTAO
    CB_GATEWAY --> CONTA
    
    PIX -.->|Circuit Breaker| BACEN
    TED -.->|Circuit Breaker| OUTROS_BANCOS
    CARTAO -.->|Circuit Breaker| ANTIFRAUDE
    CONTA -.->|Circuit Breaker| SERASA
    
    PIX --> POOL_PIX
    TED --> POOL_TED
    CONTA --> POOL_CONSULTAS
    
    CB_GATEWAY -.->|Fallback| CACHE
    CB_GATEWAY -.->|Fallback| DEFAULT_SCORES
    CB_GATEWAY -.->|Fallback| OFFLINE_VALIDATION
    
    style CB_GATEWAY fill:#fff3e0
    style CACHE fill:#e8f5e8
    style POOL_PIX fill:#f3e5f5
    style POOL_TED fill:#f3e5f5
    style POOL_CONSULTAS fill:#f3e5f5

Estados e Transições do Circuit Breaker

Implementação Detalhada dos Estados

public enum CircuitBreakerState {
    CLOSED,      // Normal - permite requisições
    OPEN,        // Proteção - rejeita requisições  
    HALF_OPEN    // Teste - permite requisições limitadas
}

@Component
public class BankingCircuitBreaker {
    
    private volatile CircuitBreakerState state = CircuitBreakerState.CLOSED;
    private final CircuitBreakerConfig config;
    private final SlidingWindow slidingWindow;
    private final AtomicLong lastFailureTime = new AtomicLong(0);
    private final AtomicInteger halfOpenSuccessCount = new AtomicInteger(0);
    private final MeterRegistry meterRegistry;
    
    public <T> T execute(String operationName, Supplier<T> operation, Supplier<T> fallback) {
        
        // Verifica se pode executar baseado no estado atual
        if (!shouldAllowRequest(operationName)) {
            recordRejection(operationName);
            return fallback.get();
        }
        
        Timer.Sample sample = Timer.start(meterRegistry);
        
        try {
            T result = operation.get();
            
            // Sucesso - registra e pode mudar estado
            recordSuccess(operationName, sample);
            onSuccess();
            
            return result;
            
        } catch (Exception e) {
            // Falha - registra e pode abrir circuit breaker
            recordFailure(operationName, sample, e);
            onFailure();
            
            return fallback.get();
        }
    }
    
    private boolean shouldAllowRequest(String operationName) {
        switch (state) {
            case CLOSED:
                return true;
                
            case OPEN:
                // Verifica se pode tentar transição para HALF_OPEN
                if (shouldAttemptReset()) {
                    transitionToHalfOpen(operationName);
                    return true;
                }
                return false;
                
            case HALF_OPEN:
                // Permite apenas número limitado de requisições
                return halfOpenSuccessCount.get() < config.getHalfOpenMaxCalls();
                
            default:
                return false;
        }
    }
    
    private boolean shouldAttemptReset() {
        long timeSinceLastFailure = System.currentTimeMillis() - lastFailureTime.get();
        return timeSinceLastFailure >= config.getWaitDurationInOpenState();
    }
    
    private void onSuccess() {
        switch (state) {
            case HALF_OPEN:
                int successCount = halfOpenSuccessCount.incrementAndGet();
                if (successCount >= config.getHalfOpenMaxCalls()) {
                    transitionToClosed("Sufficient successful calls in HALF_OPEN");
                }
                break;
                
            case CLOSED:
                // Já está no estado correto
                break;
                
            case OPEN:
                // Não deveria chegar aqui, mas se chegou, volta para CLOSED
                transitionToClosed("Unexpected success in OPEN state");
                break;
        }
    }
    
    private void onFailure() {
        lastFailureTime.set(System.currentTimeMillis());
        
        switch (state) {
            case CLOSED:
                // Verifica se deve abrir
                if (shouldTransitionToOpen()) {
                    transitionToOpen("Failure threshold exceeded");
                }
                break;
                
            case HALF_OPEN:
                // Qualquer falha em HALF_OPEN volta para OPEN
                transitionToOpen("Failure during HALF_OPEN test");
                break;
                
            case OPEN:
                // Já está aberto, mantém estado
                break;
        }
    }
    
    private boolean shouldTransitionToOpen() {
        if (!slidingWindow.isFullyPopulated()) {
            return false; // Não há dados suficientes
        }
        
        SlidingWindowMetrics metrics = slidingWindow.getMetrics();
        
        // Verifica failure rate
        if (metrics.getFailureRate() >= config.getFailureRateThreshold()) {
            return true;
        }
        
        // Verifica slow call rate
        if (metrics.getSlowCallRate() >= config.getSlowCallRateThreshold()) {
            return true;
        }
        
        return false;
    }
    
    private void transitionToOpen(String reason) {
        CircuitBreakerState previousState = state;
        state = CircuitBreakerState.OPEN;
        
        log.warn("Circuit breaker transitioning from {} to OPEN. Reason: {}", previousState, reason);
        
        meterRegistry.counter("circuit.breaker.state.transition",
            "from", previousState.name(),
            "to", "OPEN",
            "reason", reason).increment();
    }
    
    private void transitionToHalfOpen(String operationName) {
        state = CircuitBreakerState.HALF_OPEN;
        halfOpenSuccessCount.set(0);
        
        log.info("Circuit breaker transitioning to HALF_OPEN for operation: {}", operationName);
        
        meterRegistry.counter("circuit.breaker.state.transition",
            "from", "OPEN",
            "to", "HALF_OPEN",
            "operation", operationName).increment();
    }
    
    private void transitionToClosed(String reason) {
        state = CircuitBreakerState.CLOSED;
        halfOpenSuccessCount.set(0);
        slidingWindow.reset();
        
        log.info("Circuit breaker transitioning to CLOSED. Reason: {}", reason);
        
        meterRegistry.counter("circuit.breaker.state.transition",
            "to", "CLOSED",
            "reason", reason).increment();
    }
}

Configuração Flexível

@Configuration
public class CircuitBreakerConfig {
    
    @Bean
    @Qualifier("pix")
    public CircuitBreakerConfiguration pixCircuitBreakerConfig() {
        return CircuitBreakerConfiguration.builder()
            .name("pix-circuit-breaker")
            .failureRateThreshold(20.0f)      // 20% de falhas
            .slowCallRateThreshold(30.0f)     // 30% de chamadas lentas
            .slowCallDurationThreshold(Duration.ofSeconds(3))
            .waitDurationInOpenState(Duration.ofSeconds(30))
            .halfOpenMaxCalls(5)              // 5 tentativas em HALF_OPEN
            .slidingWindowType(WINDOW_TYPE.TIME_BASED)
            .slidingWindowSize(60)            // 60 segundos
            .minimumNumberOfCalls(10)         // Mínimo 10 calls para calcular
            .build();
    }
    
    @Bean
    @Qualifier("ted")
    public CircuitBreakerConfiguration tedCircuitBreakerConfig() {
        return CircuitBreakerConfiguration.builder()
            .name("ted-circuit-breaker")
            .failureRateThreshold(15.0f)      // TED mais crítico, menos tolerante
            .slowCallRateThreshold(25.0f)
            .slowCallDurationThreshold(Duration.ofSeconds(5))
            .waitDurationInOpenState(Duration.ofMinutes(2))  // Mais tempo para recuperar
            .halfOpenMaxCalls(3)              // Menos tentativas
            .slidingWindowType(WINDOW_TYPE.COUNT_BASED)
            .slidingWindowSize(50)            // Últimas 50 requisições
            .minimumNumberOfCalls(15)
            .build();
    }
    
    @Bean
    @Qualifier("consultas")
    public CircuitBreakerConfiguration consultasCircuitBreakerConfig() {
        return CircuitBreakerConfiguration.builder()
            .name("consultas-circuit-breaker")
            .failureRateThreshold(40.0f)      // Consultas menos críticas, mais tolerante
            .slowCallRateThreshold(50.0f)
            .slowCallDurationThreshold(Duration.ofSeconds(8))
            .waitDurationInOpenState(Duration.ofSeconds(15))  // Recupera mais rápido
            .halfOpenMaxCalls(10)             // Mais tentativas
            .slidingWindowType(WINDOW_TYPE.TIME_BASED)
            .slidingWindowSize(120)           // 2 minutos
            .minimumNumberOfCalls(5)          // Menos exigente
            .build();
    }
}

Sliding Window: Janelas Deslizantes

Time-based Sliding Window

public class TimeBaSedSlidingWindow implements SlidingWindow {
    
    private final int windowSizeInSeconds;
    private final ConcurrentHashMap<Long, WindowBucket> buckets = new ConcurrentHashMap<>();
    private final ReentrantLock lock = new ReentrantLock();
    
    public void recordCall(CallOutcome outcome, long durationMs) {
        long bucketKey = getCurrentBucketKey();
        
        buckets.computeIfAbsent(bucketKey, k -> new WindowBucket(k))
               .recordCall(outcome, durationMs);
        
        // Limpa buckets antigos
        cleanupOldBuckets();
    }
    
    @Override
    public SlidingWindowMetrics getMetrics() {
        lock.lock();
        try {
            long currentTime = System.currentTimeMillis();
            long windowStartTime = currentTime - (windowSizeInSeconds * 1000L);
            
            int totalCalls = 0;
            int failedCalls = 0;
            int slowCalls = 0;
            long totalDuration = 0;
            
            for (WindowBucket bucket : buckets.values()) {
                if (bucket.getTimestamp() >= windowStartTime) {
                    totalCalls += bucket.getTotalCalls();
                    failedCalls += bucket.getFailedCalls();
                    slowCalls += bucket.getSlowCalls();
                    totalDuration += bucket.getTotalDuration();
                }
            }
            
            return SlidingWindowMetrics.builder()
                .totalCalls(totalCalls)
                .failedCalls(failedCalls)
                .slowCalls(slowCalls)
                .averageDuration(totalCalls > 0 ? totalDuration / totalCalls : 0)
                .failureRate(totalCalls > 0 ? (float) failedCalls / totalCalls * 100 : 0)
                .slowCallRate(totalCalls > 0 ? (float) slowCalls / totalCalls * 100 : 0)
                .build();
        } finally {
            lock.unlock();
        }
    }
    
    private long getCurrentBucketKey() {
        return System.currentTimeMillis() / 1000; // Buckets de 1 segundo
    }
    
    private void cleanupOldBuckets() {
        long cutoffTime = System.currentTimeMillis() - (windowSizeInSeconds * 1000L);
        buckets.entrySet().removeIf(entry -> entry.getValue().getTimestamp() < cutoffTime);
    }
}

@Data
@Builder
public class WindowBucket {
    private final long timestamp;
    private final AtomicInteger totalCalls = new AtomicInteger(0);
    private final AtomicInteger failedCalls = new AtomicInteger(0);
    private final AtomicInteger slowCalls = new AtomicInteger(0);
    private final AtomicLong totalDuration = new AtomicLong(0);
    
    public void recordCall(CallOutcome outcome, long durationMs) {
        totalCalls.incrementAndGet();
        totalDuration.addAndGet(durationMs);
        
        if (outcome == CallOutcome.FAILURE) {
            failedCalls.incrementAndGet();
        }
        
        if (outcome == CallOutcome.SLOW) {
            slowCalls.incrementAndGet();
        }
    }
}

Count-based Sliding Window

public class CountBasedSlidingWindow implements SlidingWindow {
    
    private final int windowSize;
    private final CallRecord[] window;
    private final AtomicInteger currentIndex = new AtomicInteger(0);
    private final AtomicInteger totalRecords = new AtomicInteger(0);
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    
    public CountBasedSlidingWindow(int windowSize) {
        this.windowSize = windowSize;
        this.window = new CallRecord[windowSize];
    }
    
    @Override
    public void recordCall(CallOutcome outcome, long durationMs) {
        lock.writeLock().lock();
        try {
            int index = currentIndex.getAndUpdate(i -> (i + 1) % windowSize);
            window[index] = new CallRecord(outcome, durationMs, System.currentTimeMillis());
            
            if (totalRecords.get() < windowSize) {
                totalRecords.incrementAndGet();
            }
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    @Override
    public SlidingWindowMetrics getMetrics() {
        lock.readLock().lock();
        try {
            int recordCount = totalRecords.get();
            if (recordCount == 0) {
                return SlidingWindowMetrics.empty();
            }
            
            int totalCalls = 0;
            int failedCalls = 0;
            int slowCalls = 0;
            long totalDuration = 0;
            
            // Itera apenas sobre registros válidos
            for (int i = 0; i < recordCount; i++) {
                CallRecord record = window[i];
                if (record != null) {
                    totalCalls++;
                    totalDuration += record.getDurationMs();
                    
                    if (record.getOutcome() == CallOutcome.FAILURE) {
                        failedCalls++;
                    }
                    
                    if (record.getOutcome() == CallOutcome.SLOW) {
                        slowCalls++;
                    }
                }
            }
            
            return SlidingWindowMetrics.builder()
                .totalCalls(totalCalls)
                .failedCalls(failedCalls)
                .slowCalls(slowCalls)
                .averageDuration(totalCalls > 0 ? totalDuration / totalCalls : 0)
                .failureRate(totalCalls > 0 ? (float) failedCalls / totalCalls * 100 : 0)
                .slowCallRate(totalCalls > 0 ? (float) slowCalls / totalCalls * 100 : 0)
                .build();
        } finally {
            lock.readLock().unlock();
        }
    }
}

Exemplos Bancários Específicos

1. PIX com Circuit Breaker Inteligente

@Service
public class PixServiceWithCircuitBreaker {
    
    private final BacenApiClient bacenClient;
    private final BankingCircuitBreaker circuitBreaker;
    private final PixCacheService pixCache;
    private final MeterRegistry meterRegistry;
    
    public PixValidationResult validarChavePix(String chave) {
        
        return circuitBreaker.execute(
            "validar-chave-pix",
            // Operação principal
            () -> {
                log.debug("Consultando chave PIX no BACEN: {}", maskChave(chave));
                
                var response = bacenClient.consultarChave(chave);
                
                // Verifica tipos específicos de erro
                if (response.getStatus() == BacenStatus.RATE_LIMITED) {
                    throw new BacenRateLimitException("Rate limit BACEN atingido");
                }
                
                if (response.getStatus() == BacenStatus.SYSTEM_ERROR) {
                    throw new BacenSystemException("Erro interno BACEN");
                }
                
                // Sucesso - cacheia resultado
                var result = new PixValidationResult(response);
                pixCache.cache(chave, result, Duration.ofMinutes(5));
                
                return result;
            },
            // Fallback
            () -> {
                log.warn("Circuit breaker aberto para PIX. Usando fallbacks para chave: {}", maskChave(chave));
                
                // 1. Tenta cache local
                var cachedResult = pixCache.get(chave);
                if (cachedResult.isPresent()) {
                    meterRegistry.counter("pix.fallback.cache.hit").increment();
                    return cachedResult.get().withWarning("Dados do cache - BACEN indisponível");
                }
                
                // 2. Validação local básica
                if (isValidPixKeyFormat(chave)) {
                    meterRegistry.counter("pix.fallback.local.validation").increment();
                    return PixValidationResult.localValidation(chave);
                }
                
                // 3. Último recurso - rejeita operação
                meterRegistry.counter("pix.fallback.rejected").increment();
                throw new PixValidationUnavailableException("Validação PIX temporariamente indisponível");
            }
        );
    }
    
    private boolean isValidPixKeyFormat(String chave) {
        // Validações locais: CPF, CNPJ, email, telefone, chave aleatória
        return PixKeyValidator.isValidFormat(chave);
    }
}

2. TED com Circuit Breaker Hierárquico

@Service
public class TedServiceWithHierarchicalCircuitBreaker {
    
    private final Map<String, BankingCircuitBreaker> bankCircuitBreakers;
    private final BankingCircuitBreaker globalTedCircuitBreaker;
    private final InterbankApiClient interbankClient;
    
    public TedConfirmation processarTed(TedRequest request) {
        
        String bancoDest = request.getBancoDestino();
        
        // 1. Verifica circuit breaker global de TED
        if (!globalTedCircuitBreaker.isCallPermitted()) {
            log.warn("Circuit breaker global de TED aberto");
            throw new TedGloballyUnavailableException("Serviço TED temporariamente indisponível");
        }
        
        // 2. Verifica circuit breaker específico do banco destino
        BankingCircuitBreaker bankCircuitBreaker = getBankCircuitBreaker(bancoDest);
        
        return bankCircuitBreaker.execute(
            "ted-" + bancoDest,
            // Operação principal
            () -> {
                return interbankClient.processarTed(request);
            },
            // Fallback específico por banco
            () -> {
                return handleTedFallback(request, bancoDest);
            }
        );
    }
    
    private BankingCircuitBreaker getBankCircuitBreaker(String bancoCodigo) {
        return bankCircuitBreakers.computeIfAbsent(bancoCodigo, banco -> {
            
            // Configuração específica por banco
            CircuitBreakerConfiguration config = getBankSpecificConfig(banco);
            
            return BankingCircuitBreaker.of(config);
        });
    }
    
    private CircuitBreakerConfiguration getBankSpecificConfig(String bancoCodigo) {
        // Bancos grandes - mais tolerantes (têm infraestrutura melhor)
        if (isBigBank(bancoCodigo)) {
            return CircuitBreakerConfiguration.builder()
                .failureRateThreshold(25.0f)      // Mais tolerante
                .waitDurationInOpenState(Duration.ofSeconds(30))
                .build();
        }
        
        // Bancos menores - menos tolerantes (podem ter instabilidade)
        return CircuitBreakerConfiguration.builder()
            .failureRateThreshold(15.0f)          // Menos tolerante
            .waitDurationInOpenState(Duration.ofMinutes(2))
            .build();
    }
    
    private TedConfirmation handleTedFallback(TedRequest request, String bancoDest) {
        
        // 1. TED via canal alternativo (se disponível)
        if (hasAlternativeChannel(bancoDest)) {
            try {
                return processViaAlternativeChannel(request);
            } catch (Exception e) {
                log.warn("Canal alternativo também falhou para banco {}: {}", bancoDest, e.getMessage());
            }
        }
        
        // 2. Agenda para processamento posterior
        tedSchedulingService.scheduleForLater(request, Duration.ofMinutes(30));
        
        return TedConfirmation.scheduled(
            request.getId(),
            "TED agendada para processamento - banco destino temporariamente indisponível"
        );
    }
}

3. Consulta de Score com Bulkhead Pattern

@Service
public class ScoreServiceWithBulkhead {
    
    private final ThreadPoolTaskExecutor serasaExecutor;
    private final ThreadPoolTaskExecutor spcExecutor;
    private final ThreadPoolTaskExecutor scRExecutor;
    private final BankingCircuitBreaker serasaCircuitBreaker;
    private final BankingCircuitBreaker spcCircuitBreaker;
    private final BankingCircuitBreaker scrCircuitBreaker;
    
    @PostConstruct
    public void setupBulkheads() {
        // Bulk SERASA - isolado
        serasaExecutor = new ThreadPoolTaskExecutor();
        serasaExecutor.setCorePoolSize(5);
        serasaExecutor.setMaxPoolSize(10);
        serasaExecutor.setQueueCapacity(50);
        serasaExecutor.setThreadNamePrefix("serasa-");
        serasaExecutor.setRejectedExecutionHandler(new CallerRunsPolicy());
        serasaExecutor.initialize();
        
        // Bulkhead SPC - isolado
        spcExecutor = new ThreadPoolTaskExecutor();
        spcExecutor.setCorePoolSize(3);
        spcExecutor.setMaxPoolSize(8);
        spcExecutor.setQueueCapacity(30);
        spcExecutor.setThreadNamePrefix("spc-");
        spcExecutor.setRejectedExecutionHandler(new CallerRunsPolicy());
        spcExecutor.initialize();
        
        // Bulkhead SCR - isolado
        scrExecutor = new ThreadPoolTaskExecutor();
        scrExecutor.setCorePoolSize(2);
        scrExecutor.setMaxPoolSize(5);
        scrExecutor.setQueueCapacity(20);
        scrExecutor.setThreadNamePrefix("scr-");
        scrExecutor.setRejectedExecutionHandler(new CallerRunsPolicy());
        scrExecutor.initialize();
    }
    
    public CompletableFuture<ScoreConsolidado> consultarScoreCompleto(String cpf) {
        
        // Consultas paralelas com isolamento
        CompletableFuture<ScoreData> serasaFuture = consultarSerasaAsync(cpf);
        CompletableFuture<ScoreData> spcFuture = consultarSpcAsync(cpf);
        CompletableFuture<ScoreData> scrFuture = consultarScrAsync(cpf);
        
        // Combina resultados com timeout
        return CompletableFuture.allOf(serasaFuture, spcFuture, scrFuture)
            .thenApply(v -> {
                ScoreData serasa = serasaFuture.getNow(null);
                ScoreData spc = spcFuture.getNow(null);
                ScoreData scr = scrFuture.getNow(null);
                
                return consolidarScores(serasa, spc, scr);
            })
            .orTimeout(10, TimeUnit.SECONDS)
            .exceptionally(throwable -> {
                log.error("Erro na consulta consolidada de score para CPF {}: {}", 
                    maskCpf(cpf), throwable.getMessage());
                
                return ScoreConsolidado.fallback(cpf);
            });
    }
    
    private CompletableFuture<ScoreData> consultarSerasaAsync(String cpf) {
        return CompletableFuture.supplyAsync(() -> 
            serasaCircuitBreaker.execute(
                "consulta-serasa",
                () -> serasaApiClient.consultarScore(cpf),
                () -> ScoreData.unavailable("SERASA", "Circuit breaker aberto")
            ), serasaExecutor
        );
    }
    
    private CompletableFuture<ScoreData> consultarSpcAsync(String cpf) {
        return CompletableFuture.supplyAsync(() -> 
            spcCircuitBreaker.execute(
                "consulta-spc",
                () -> spcApiClient.consultarScore(cpf),
                () -> ScoreData.unavailable("SPC", "Circuit breaker aberto")
            ), spcExecutor
        );
    }
    
    private CompletableFuture<ScoreData> consultarScrAsync(String cpf) {
        return CompletableFuture.supplyAsync(() -> 
            scrCircuitBreaker.execute(
                "consulta-scr",
                () -> scrApiClient.consultarScore(cpf),
                () -> ScoreData.unavailable("SCR", "Circuit breaker aberto")
            ), scrExecutor
        );
    }
    
    private ScoreConsolidado consolidarScores(ScoreData serasa, ScoreData spc, ScoreData scr) {
        
        List<ScoreData> validScores = Stream.of(serasa, spc, scr)
            .filter(Objects::nonNull)
            .filter(ScoreData::isValid)
            .collect(Collectors.toList());
        
        if (validScores.isEmpty()) {
            return ScoreConsolidado.fallback("Todos os bureaus indisponíveis");
        }
        
        // Algoritmo de consolidação
        int scoreConsolidado = (int) validScores.stream()
            .mapToInt(ScoreData::getScore)
            .average()
            .orElse(500); // Fallback conservador
        
        return ScoreConsolidado.builder()
            .cpf(serasa != null ? serasa.getCpf() : spc != null ? spc.getCpf() : scr.getCpf())
            .scoreConsolidado(scoreConsolidado)
            .fontesConsultadas(validScores.size())
            .detalhes(validScores)
            .dataConsulta(Instant.now())
            .build();
    }
}

Rate Limiting Integrado

Rate Limiter com Circuit Breaker

@Component
public class AdaptiveRateLimiter {
    
    private final BankingCircuitBreaker circuitBreaker;
    private final TokenBucket tokenBucket;
    private final AtomicInteger currentRate = new AtomicInteger();
    private final AtomicInteger baseRate;
    
    public AdaptiveRateLimiter(int baseRatePerSecond) {
        this.baseRate = new AtomicInteger(baseRatePerSecond);
        this.currentRate.set(baseRatePerSecond);
        this.tokenBucket = TokenBucket.of(baseRatePerSecond);
        
        // Ajusta rate baseado no estado do circuit breaker
        setupCircuitBreakerListener();
    }
    
    public boolean tryAcquire(String operationName) {
        
        // Se circuit breaker está aberto, reduz drasticamente o rate
        if (circuitBreaker.getState() == CircuitBreakerState.OPEN) {
            int reducedRate = (int) (baseRate.get() * 0.1); // 10% do rate normal
            return tokenBucket.tryConsume(1, reducedRate);
        }
        
        // Se circuit breaker está HALF_OPEN, reduz moderadamente
        if (circuitBreaker.getState() == CircuitBreakerState.HALF_OPEN) {
            int reducedRate = (int) (baseRate.get() * 0.5); // 50% do rate normal
            return tokenBucket.tryConsume(1, reducedRate);
        }
        
        // Estado normal
        return tokenBucket.tryConsume(1);
    }
    
    private void setupCircuitBreakerListener() {
        circuitBreaker.getEventPublisher()
            .onStateTransition(event -> {
                switch (event.getStateTransition().getToState()) {
                    case OPEN:
                        // Reduz rate drasticamente
                        currentRate.set((int) (baseRate.get() * 0.1));
                        log.info("Rate limiting reduzido para {} req/s devido ao circuit breaker OPEN", 
                            currentRate.get());
                        break;
                        
                    case HALF_OPEN:
                        // Reduz rate moderadamente
                        currentRate.set((int) (baseRate.get() * 0.5));
                        log.info("Rate limiting reduzido para {} req/s devido ao circuit breaker HALF_OPEN", 
                            currentRate.get());
                        break;
                        
                    case CLOSED:
                        // Restaura rate normal
                        currentRate.set(baseRate.get());
                        log.info("Rate limiting restaurado para {} req/s - circuit breaker CLOSED", 
                            currentRate.get());
                        break;
                }
                
                tokenBucket.updateRate(currentRate.get());
            });
    }
}

@Service
public class BankingApiWithRateLimit {
    
    private final AdaptiveRateLimiter pixRateLimiter;
    private final AdaptiveRateLimiter tedRateLimiter;
    private final AdaptiveRateLimiter consultasRateLimiter;
    
    public BankingApiWithRateLimit() {
        // Rate limits diferenciados por criticidade
        this.pixRateLimiter = new AdaptiveRateLimiter(1000);        // PIX: 1000 req/s
        this.tedRateLimiter = new AdaptiveRateLimiter(500);         // TED: 500 req/s
        this.consultasRateLimiter = new AdaptiveRateLimiter(2000);  // Consultas: 2000 req/s
    }
    
    @PostMapping("/pix")
    public ResponseEntity<PixResponse> processarPix(@RequestBody PixRequest request) {
        
        if (!pixRateLimiter.tryAcquire("pix-operation")) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                .header("Retry-After", "1")
                .body(PixResponse.rateLimited());
        }
        
        return ResponseEntity.ok(pixService.processar(request));
    }
    
    @PostMapping("/ted")
    public ResponseEntity<TedResponse> processarTed(@RequestBody TedRequest request) {
        
        if (!tedRateLimiter.tryAcquire("ted-operation")) {
            return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
                .header("Retry-After", "2")
                .body(TedResponse.rateLimited());
        }
        
        return ResponseEntity.ok(tedService.processar(request));
    }
}

Thresholds Adaptativos

Circuit Breaker que Aprende

@Component
public class AdaptiveCircuitBreaker extends BankingCircuitBreaker {
    
    private final CircuitBreakerMetricsRepository metricsRepository;
    private final MachineLearningService mlService;
    
    @Scheduled(fixedDelay = 300000) // A cada 5 minutos
    public void adaptThresholds() {
        
        // Coleta métricas históricas
        var historicalMetrics = metricsRepository.getMetricsLastHours(24);
        
        // Analisa padrões
        var analysis = mlService.analyzeFailurePatterns(historicalMetrics);
        
        // Ajusta thresholds baseado no aprendizado
        if (analysis.shouldAdjustThresholds()) {
            adaptFailureRateThreshold(analysis);
            adaptSlowCallThreshold(analysis);
            adaptWaitDuration(analysis);
        }
    }
    
    private void adaptFailureRateThreshold(FailurePatternAnalysis analysis) {
        
        float currentThreshold = config.getFailureRateThreshold();
        float suggestedThreshold = analysis.getSuggestedFailureRateThreshold();
        
        // Ajuste gradual (máximo 5% por vez)
        float maxChange = 5.0f;
        float newThreshold = Math.max(
            Math.min(suggestedThreshold, currentThreshold + maxChange),
            currentThreshold - maxChange
        );
        
        // Aplica apenas se mudança for significativa
        if (Math.abs(newThreshold - currentThreshold) > 2.0f) {
            config.setFailureRateThreshold(newThreshold);
            
            log.info("Threshold de failure rate ajustado de {}% para {}% baseado em análise ML", 
                currentThreshold, newThreshold);
                
            meterRegistry.gauge("circuit.breaker.threshold.failure.rate", newThreshold);
        }
    }
    
    private void adaptSlowCallThreshold(FailurePatternAnalysis analysis) {
        
        // Analisa distribuição de latências
        var latencyDistribution = analysis.getLatencyDistribution();
        
        // Ajusta threshold para P95 + margem
        Duration newSlowCallThreshold = latencyDistribution.getPercentile(95)
            .plus(Duration.ofMillis(500)); // Margem de 500ms
        
        Duration currentThreshold = config.getSlowCallDurationThreshold();
        
        // Limita mudanças drásticas
        if (newSlowCallThreshold.toMillis() > currentThreshold.toMillis() * 2) {
            newSlowCallThreshold = currentThreshold.multipliedBy(2);
        }
        
        if (newSlowCallThreshold.toMillis() < currentThreshold.toMillis() / 2) {
            newSlowCallThreshold = currentThreshold.dividedBy(2);
        }
        
        config.setSlowCallDurationThreshold(newSlowCallThreshold);
        
        log.info("Threshold de slow call ajustado de {}ms para {}ms", 
            currentThreshold.toMillis(), newSlowCallThreshold.toMillis());
    }
    
    private void adaptWaitDuration(FailurePatternAnalysis analysis) {
        
        // Analisa tempo médio de recuperação histórico
        Duration avgRecoveryTime = analysis.getAverageRecoveryTime();
        
        // Ajusta wait duration para 80% do tempo médio de recuperação
        Duration newWaitDuration = avgRecoveryTime.multipliedBy(80).dividedBy(100);
        
        // Limites mínimo e máximo
        newWaitDuration = Duration.ofMillis(Math.max(
            Math.min(newWaitDuration.toMillis(), 300000), // Máx 5 min
            10000  // Mín 10 seg
        ));
        
        config.setWaitDurationInOpenState(newWaitDuration);
        
        log.info("Wait duration ajustado para {}s baseado em tempo médio de recuperação", 
            newWaitDuration.getSeconds());
    }
}

Observabilidade Avançada

Métricas Detalhadas

@Component
public class CircuitBreakerMetrics {
    
    private final MeterRegistry meterRegistry;
    private final Map<String, Timer> operationTimers = new ConcurrentHashMap<>();
    private final Map<String, Counter> stateTransitionCounters = new ConcurrentHashMap<>();
    
    public void recordCircuitBreakerOperation(String operationName, 
                                            CircuitBreakerState state,
                                            long durationMs, 
                                            boolean success) {
        
        // Timer por operação e estado
        String timerName = String.format("circuit.breaker.operation.%s.%s", 
            operationName, state.name().toLowerCase());
        
        Timer timer = operationTimers.computeIfAbsent(timerName, 
            name -> Timer.builder("circuit.breaker.operation.duration")
                .tag("operation", operationName)
                .tag("state", state.name())
                .tag("result", success ? "success" : "failure")
                .register(meterRegistry));
        
        timer.record(durationMs, TimeUnit.MILLISECONDS);
        
        // Contador de tentativas por estado
        meterRegistry.counter("circuit.breaker.attempts",
            "operation", operationName,
            "state", state.name(),
            "result", success ? "success" : "failure").increment();
        
        // Gauge do estado atual
        meterRegistry.gauge("circuit.breaker.current.state",
            Tags.of("operation", operationName),
            state, s -> s.ordinal());
    }
    
    public void recordStateTransition(String operationName, 
                                    CircuitBreakerState fromState, 
                                    CircuitBreakerState toState,
                                    String reason) {
        
        meterRegistry.counter("circuit.breaker.state.transitions",
            "operation", operationName,
            "from", fromState.name(),
            "to", toState.name(),
            "reason", reason).increment();
        
        // Métricas especiais para transições críticas
        if (toState == CircuitBreakerState.OPEN) {
            meterRegistry.counter("circuit.breaker.opened",
                "operation", operationName,
                "reason", reason).increment();
        }
        
        if (fromState == CircuitBreakerState.OPEN && toState == CircuitBreakerState.CLOSED) {
            meterRegistry.counter("circuit.breaker.recovered",
                "operation", operationName).increment();
        }
    }
    
    public void recordFallbackExecution(String operationName, String fallbackType) {
        meterRegistry.counter("circuit.breaker.fallback.executed",
            "operation", operationName,
            "fallback_type", fallbackType).increment();
    }
    
    @EventListener
    public void onCircuitBreakerEvent(CircuitBreakerEvent event) {
        switch (event.getEventType()) {
            case STATE_TRANSITION:
                handleStateTransition((CircuitBreakerOnStateTransitionEvent) event);
                break;
            case CALL_NOT_PERMITTED:
                handleCallNotPermitted((CircuitBreakerOnCallNotPermittedEvent) event);
                break;
            case ERROR:
                handleError((CircuitBreakerOnErrorEvent) event);
                break;
            case SUCCESS:
                handleSuccess((CircuitBreakerOnSuccessEvent) event);
                break;
        }
    }
}

Dashboard Específico para Banking

-- Queries Grafana para Circuit Breakers Bancários

-- 1. Estado atual de todos os circuit breakers
circuit_breaker_current_state

-- 2. Taxa de falhas por operação bancária (últimos 5 min)
(
  rate(circuit_breaker_attempts_total{result="failure"}[5m]) /
  rate(circuit_breaker_attempts_total[5m])
) * 100

-- 3. Duração P95 por operação em cada estado
histogram_quantile(0.95, 
  rate(circuit_breaker_operation_duration_bucket[5m])
)

-- 4. Número de aberturas por operação (últimas 24h)
increase(circuit_breaker_opened_total[24h])

-- 5. Tempo médio para recuperação
avg(
  circuit_breaker_recovery_time_seconds
) by (operation)

-- 6. Execuções de fallback por tipo
rate(circuit_breaker_fallback_executed_total[5m])

-- 7. Top operações com mais falhas
topk(10,
  sum(rate(circuit_breaker_attempts_total{result="failure"}[5m])) by (operation)
)

Alertas Críticos Específicos

# circuit-breaker-alerts.yml
groups:
  - name: circuit-breaker-banking
    rules:
      - alert: PixCircuitBreakerOpen
        expr: circuit_breaker_current_state{operation="pix"} == 2  # OPEN = 2
        for: 30s
        labels:
          severity: critical
          team: payments
        annotations:
          summary: "Circuit breaker do PIX está aberto"
          description: "PIX indisponível há {{ $for }}. Impacto em transferências instantâneas."
      
      - alert: TedCircuitBreakerOpen
        expr: circuit_breaker_current_state{operation="ted"} == 2
        for: 1m
        labels:
          severity: critical
          team: payments
        annotations:
          summary: "Circuit breaker do TED está aberto"
          description: "TED indisponível há {{ $for }}. Transferências sendo agendadas."
      
      - alert: HighCircuitBreakerFailureRate
        expr: |
          (
            rate(circuit_breaker_attempts_total{result="failure"}[5m]) /
            rate(circuit_breaker_attempts_total[5m])
          ) * 100 > 15
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Alta taxa de falhas para {{ $labels.operation }}"
          description: "{{ $labels.operation }} com {{ $value }}% de falhas"
      
      - alert: CircuitBreakerFlapping
        expr: |
          increase(circuit_breaker_state_transitions_total[10m]) > 5
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Circuit breaker oscilando para {{ $labels.operation }}"
          description: "{{ $value }} transições de estado em 10 minutos"
      
      - alert: FallbackExecutionSpike
        expr: |
          rate(circuit_breaker_fallback_executed_total[5m]) > 50
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "Spike de execuções de fallback"
          description: "{{ $value }} fallbacks/segundo para {{ $labels.operation }}"

Estratégias de Fallback por Contexto

Fallbacks Inteligentes

@Component
public class BankingFallbackStrategies {
    
    private final CacheService cacheService;
    private final DefaultValueService defaultValueService;
    private final AlternativeServiceClient alternativeServiceClient;
    
    public PixValidationResult pixFallback(String chave, Exception lastException) {
        
        // 1. Cache local (mais recente)
        var cached = cacheService.getPixValidation(chave);
        if (cached.isPresent() && !cached.get().isExpired(Duration.ofHours(1))) {
            return cached.get().withWarning("Dados em cache - BACEN indisponível");
        }
        
        // 2. Validação local básica
        if (PixKeyValidator.isValidFormat(chave)) {
            return PixValidationResult.builder()
                .chave(chave)
                .status(PixKeyStatus.FORMAT_VALID)
                .warning("Validação local - BACEN indisponível")
                .build();
        }
        
        // 3. Último recurso - rejeita
        throw new PixUnavailableException("Validação PIX indisponível", lastException);
    }
    
    public ScoreData scoreFallback(String cpf, String fonte, Exception lastException) {
        
        // 1. Cache (aceita até dados de 24h)
        var cached = cacheService.getScore(cpf, fonte);
        if (cached.isPresent()) {
            return cached.get().withWarning("Score em cache de " + cached.get().getAge());
        }
        
        // 2. Score baseado em dados internos
        var internalScore = calculateInternalScore(cpf);
        if (internalScore.isPresent()) {
            return ScoreData.builder()
                .cpf(cpf)
                .score(internalScore.get())
                .fonte("INTERNAL")
                .warning("Score calculado internamente - " + fonte + " indisponível")
                .build();
        }
        
        // 3. Score conservador padrão
        return ScoreData.builder()
            .cpf(cpf)
            .score(500)  // Score neutro
            .fonte("DEFAULT")
            .warning("Score padrão - " + fonte + " indisponível")
            .build();
    }
    
    public TedConfirmation tedFallback(TedRequest request, String bancoDest, Exception lastException) {
        
        // 1. Canal alternativo (se disponível)
        if (hasAlternativeChannel(bancoDest)) {
            try {
                return alternativeServiceClient.processarTed(request);
            } catch (Exception e) {
                log.warn("Canal alternativo também falhou: {}", e.getMessage());
            }
        }
        
        // 2. Agendamento para processamento posterior
        var scheduledTime = calculateNextAvailableTime(bancoDest);
        tedSchedulingService.scheduleFor(request, scheduledTime);
        
        return TedConfirmation.builder()
            .id(request.getId())
            .status(TedStatus.SCHEDULED)
            .scheduledFor(scheduledTime)
            .message("TED agendada para " + formatTime(scheduledTime) + 
                    " - banco destino temporariamente indisponível")
            .build();
    }
    
    public AntiFraudResult antiFraudFallback(AntiFraudRequest request, Exception lastException) {
        
        // Antifraude: sempre conservador quando indisponível
        
        // 1. Análise local simples
        var localAnalysis = performLocalFraudAnalysis(request);
        
        // 2. Baseado no valor e histórico do cliente
        if (request.getValor().compareTo(new BigDecimal("1000")) > 0) {
            // Valores altos: requer aprovação manual
            return AntiFraudResult.builder()
                .status(AntiFraudStatus.MANUAL_REVIEW)
                .riskScore(0.7f)  // Score conservador
                .reason("Análise manual necessária - antifraude indisponível")
                .build();
        }
        
        // 3. Valores baixos: aprova com monitoramento
        return AntiFraudResult.builder()
            .status(AntiFraudStatus.APPROVED)
            .riskScore(0.3f)
            .reason("Aprovação local - antifraude indisponível")
            .monitoring(true)  // Flag para monitoramento extra
            .build();
    }
    
    private Optional<Integer> calculateInternalScore(String cpf) {
        try {
            // Score baseado em histórico interno do cliente
            var clientHistory = clientHistoryService.getHistory(cpf);
            
            if (clientHistory.isEmpty()) {
                return Optional.empty();
            }
            
            // Algoritmo simples baseado em:
            // - Tempo de relacionamento
            // - Movimentação média
            // - Inadimplência histórica
            
            int score = 600; // Base
            
            score += Math.min(clientHistory.getRelationshipMonths() * 2, 100);
            score += Math.min((int) (clientHistory.getAvgMonthlyMovement().doubleValue() / 1000), 50);
            score -= clientHistory.getDefaultCount() * 50;
            
            return Optional.of(Math.max(Math.min(score, 950), 200));
            
        } catch (Exception e) {
            log.warn("Erro ao calcular score interno para CPF {}: {}", maskCpf(cpf), e.getMessage());
            return Optional.empty();
        }
    }
}

Conclusão

Circuit Breakers são fundamentais para proteger sistemas bancários contra falhas em cascata. A implementação deve considerar estados adaptativos, sliding windows inteligentes, e fallbacks específicos por contexto de negócio.

A integração com Rate Limiting e Bulkhead Pattern cria camadas de proteção complementares. Observabilidade detalhada permite otimização contínua dos thresholds baseada em padrões reais de falha.

Benefícios alcançados:

  • Isolamento: Falhas não se propagam entre serviços
  • Graceful Degradation: Sistema funciona mesmo com limitações
  • Recuperação Automática: Volta ao normal quando possível
  • Experiência do Usuário: Fallbacks transparentes
  • Observabilidade: Visibilidade completa sobre estado do sistema

Próximos passos:

No próximo artigo, exploraremos Cache Patterns e Performance em sistemas bancários, incluindo cache hierárquico, invalidação inteligente e estratégias de warm-up para operações críticas.