Menu

Retry com Backoff: Resiliência em Sistemas Distribuídos

Como implementar estratégias inteligentes de retry para garantir robustez em operações distribuídas críticas

Retry com Backoff: Resiliência em Sistemas Distribuídos

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

O Problema: Quando Sistemas “Ficam Ocupados”

Imagine que você está fazendo um PIX de R$ 500:

Situação 1 - Sistema Sobrecarregado:

  • Você aperta “confirmar PIX”
  • Sistema precisa validar dados, calcular valores, processar transação
  • Tela fica carregando… carregando…
  • “Erro temporário, tente novamente”
  • Sistema tenta de novo IMEDIATAMENTE
  • Erro de novo… e de novo… e de novo…

Problema: Sistema está “bombardeando” serviços que já estão sobrecarregados!

A Solução: Retry Inteligente

Sistema Moderno com Retry Inteligente:

  • 1ª tentativa: Processa PIX → Falha imediata
  • 2ª tentativa: Espera 1 segundo → Tenta novamente
  • 3ª tentativa: Espera 2 segundos → Nova tentativa
  • 4ª tentativa: Espera 4 segundos → Mais uma tentativa
  • 5ª tentativa: Espera 8 segundos → Última tentativa

Resultado: Sistemas têm tempo para “respirar” e a operação tem mais chance de dar certo!

Analogia do Call Center

É como ligar para o atendimento do banco:

Jeito Errado (sem backoff):

  • 14:00:00 - Liga: “Todas as linhas ocupadas”
  • 14:00:01 - Liga: “Todas as linhas ocupadas”
  • 14:00:02 - Liga: “Todas as linhas ocupadas”
  • Resultado: Você entope ainda mais as linhas!

Jeito Certo (com backoff):

  • 14:00:00 - Liga: “Todas as linhas ocupadas”
  • 14:01:00 - Liga: “Todas as linhas ocupadas”
  • 14:03:00 - Liga: “Todas as linhas ocupadas”
  • 14:07:00 - Liga: **“Olá, como posso ajudar?""

Tipos de “Espera Inteligente”

Linear (sempre o mesmo tempo):

  • Espera 2s, 2s, 2s, 2s…
  • Como bater na porta de 2 em 2 segundos

Exponencial (dobra o tempo):

  • Espera 1s, 2s, 4s, 8s, 16s…
  • Como dar mais e mais espaço para o sistema se recuperar

Com Jitter (aleatório):

  • Espera 1s, 3s, 7s, 12s…
  • Como evitar que todo mundo tente ao mesmo tempo

Por que isso é crucial no banco?

  • PIX: Milhões de transações simultâneas
  • 💳 Cartão: Validação em milissegundos com lojas
  • Consultas: CPF no SERASA, SPC, BACEN
  • Integrações: Outros bancos, fintechs, marketplaces

Sem retry inteligente: Sistemas colapsam em horários de pico Com retry inteligente: Operações funcionam mesmo sob pressão

Conceitos técnicos

O Desafio das Falhas Transitórias

Em sistemas bancários, nem toda falha é permanente. Muitas são transitórias:

  • Sobrecarga temporária: Pico de transações PIX no Black Friday
  • Latência de rede: Consulta ao BACEN com delay momentâneo
  • Rate limiting: APIs externas limitando requisições por segundo
  • Recursos esgotados: Pool de conexões temporariamente cheio

Estratégias de Backoff

Exponential Backoff: Tempo de espera cresce exponencialmente (2^retry * base_delay) Linear Backoff: Incremento fixo no tempo de espera Jitter: Adiciona aleatoriedade para evitar “thundering herd” Circuit Breaker: Para tentativas quando falhas são consecutivas

Implementação: Estratégias de Retry

1. Exponential Backoff Básico

@Component
public class ExponentialBackoffRetry {
    
    private static final int MAX_RETRIES = 5;
    private static final long BASE_DELAY_MS = 1000; // 1 segundo
    private static final long MAX_DELAY_MS = 30000; // 30 segundos
    
    public <T> T executeWithRetry(Supplier<T> operation, String operationName) {
        Exception lastException = null;
        
        for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
            try {
                T result = operation.get();
                
                if (attempt > 0) {
                    log.info("Operação {} sucedeu na tentativa {}", operationName, attempt + 1);
                }
                
                return result;
                
            } catch (Exception e) {
                lastException = e;
                
                if (attempt == MAX_RETRIES) {
                    log.error("Operação {} falhou após {} tentativas", operationName, MAX_RETRIES + 1);
                    break;
                }
                
                long delay = calculateExponentialDelay(attempt);
                log.warn("Operação {} falhou na tentativa {}. Tentando novamente em {}ms. Erro: {}", 
                    operationName, attempt + 1, delay, e.getMessage());
                
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrompido", ie);
                }
            }
        }
        
        throw new MaxRetriesExceededException("Máximo de tentativas excedido para " + operationName, lastException);
    }
    
    private long calculateExponentialDelay(int attempt) {
        long delay = (long) (BASE_DELAY_MS * Math.pow(2, attempt));
        return Math.min(delay, MAX_DELAY_MS);
    }
}

2. Retry com Jitter (Anti-Thundering Herd)

@Component
public class JitterBackoffRetry {
    
    private final Random random = new Random();
    
    public <T> T executeWithJitter(Supplier<T> operation, String operationName) {
        Exception lastException = null;
        
        for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
            try {
                return operation.get();
                
            } catch (Exception e) {
                lastException = e;
                
                if (attempt == MAX_RETRIES) break;
                
                long delay = calculateJitterDelay(attempt);
                sleepWithInterruptCheck(delay);
            }
        }
        
        throw new MaxRetriesExceededException("Falhou após " + (MAX_RETRIES + 1) + " tentativas", lastException);
    }
    
    private long calculateJitterDelay(int attempt) {
        // Exponential + Full Jitter
        long exponentialDelay = (long) (BASE_DELAY_MS * Math.pow(2, attempt));
        long cappedDelay = Math.min(exponentialDelay, MAX_DELAY_MS);
        
        // Jitter: 0 a cappedDelay
        return random.nextLong(cappedDelay + 1);
    }
    
    private void sleepWithInterruptCheck(long delayMs) {
        try {
            Thread.sleep(delayMs);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Retry interrompido", e);
        }
    }
}

3. Retry Condicional (Smart Retry)

@Component
public class SmartRetry {
    
    private final MeterRegistry meterRegistry;
    
    public <T> T executeSmartRetry(Supplier<T> operation, String operationName, RetryConfig config) {
        Timer.Sample sample = Timer.start(meterRegistry);
        Exception lastException = null;
        
        for (int attempt = 0; attempt <= config.getMaxRetries(); attempt++) {
            try {
                T result = operation.get();
                
                // Métricas de sucesso
                sample.stop(Timer.builder("retry.operation.duration")
                    .tag("operation", operationName)
                    .tag("result", "success")
                    .tag("attempt", String.valueOf(attempt + 1))
                    .register(meterRegistry));
                
                meterRegistry.counter("retry.attempts.total",
                    "operation", operationName,
                    "result", "success").increment();
                
                return result;
                
            } catch (Exception e) {
                lastException = e;
                
                // Verifica se deve fazer retry
                if (!shouldRetry(e, attempt, config)) {
                    break;
                }
                
                long delay = config.getBackoffStrategy().calculateDelay(attempt);
                
                log.warn("Tentativa {} falhou para {}: {}. Próxima tentativa em {}ms", 
                    attempt + 1, operationName, e.getMessage(), delay);
                
                sleepWithInterruptCheck(delay);
                
                meterRegistry.counter("retry.attempts.total",
                    "operation", operationName,
                    "result", "retry").increment();
            }
        }
        
        // Métricas de falha
        sample.stop(Timer.builder("retry.operation.duration")
            .tag("operation", operationName)
            .tag("result", "failed")
            .register(meterRegistry));
        
        meterRegistry.counter("retry.attempts.total",
            "operation", operationName,
            "result", "failed").increment();
        
        throw new MaxRetriesExceededException("Operação " + operationName + " falhou definitivamente", lastException);
    }
    
    private boolean shouldRetry(Exception e, int attempt, RetryConfig config) {
        // Não retry se atingiu máximo
        if (attempt >= config.getMaxRetries()) {
            return false;
        }
        
        // Não retry para erros não-retryable
        if (e instanceof IllegalArgumentException || 
            e instanceof SecurityException ||
            e instanceof AuthenticationException) {
            log.warn("Erro não-retryable: {}. Não tentando novamente.", e.getClass().getSimpleName());
            return false;
        }
        
        // Retry para erros transitórios
        if (e instanceof SocketTimeoutException ||
            e instanceof ConnectException ||
            e instanceof HttpRetryableException ||
            e instanceof TemporaryResourceException) {
            return true;
        }
        
        // Verificar por HTTP status codes
        if (e instanceof HttpClientException httpEx) {
            int statusCode = httpEx.getStatusCode();
            
            // Retry para 5xx (server errors) mas não para 4xx (client errors)
            return statusCode >= 500 && statusCode < 600;
        }
        
        return config.isRetryByDefault();
    }
}

// Configuração flexível
public class RetryConfig {
    private final int maxRetries;
    private final BackoffStrategy backoffStrategy;
    private final boolean retryByDefault;
    private final Set<Class<? extends Exception>> retryableExceptions;
    private final Set<Class<? extends Exception>> nonRetryableExceptions;
    
    // builder pattern, getters...
}

// Estratégias plugáveis
public interface BackoffStrategy {
    long calculateDelay(int attempt);
}

public class ExponentialBackoffStrategy implements BackoffStrategy {
    private final long baseDelayMs;
    private final long maxDelayMs;
    private final double multiplier;
    private final boolean jitter;
    private final Random random = new Random();
    
    @Override
    public long calculateDelay(int attempt) {
        long delay = (long) (baseDelayMs * Math.pow(multiplier, attempt));
        delay = Math.min(delay, maxDelayMs);
        
        if (jitter) {
            // Full jitter: 0 to calculated delay
            delay = random.nextLong(delay + 1);
        }
        
        return delay;
    }
}

Exemplos Bancários Práticos

1. PIX: Consulta BACEN com Retry

@Service
public class PixService {
    
    private final BacenApiClient bacenClient;
    private final SmartRetry retryService;
    
    public PixValidationResult validarChavePix(String chave) {
        RetryConfig config = RetryConfig.builder()
            .maxRetries(3)
            .backoffStrategy(new ExponentialBackoffStrategy(500, 5000, 2.0, true))
            .retryableExceptions(Set.of(
                SocketTimeoutException.class,
                BacenTemporaryUnavailableException.class,
                BacenRateLimitException.class
            ))
            .nonRetryableExceptions(Set.of(
                PixChaveInvalidaException.class,
                PixChaveNaoEncontradaException.class
            ))
            .build();
        
        return retryService.executeSmartRetry(
            () -> {
                log.debug("Consultando chave PIX no BACEN: {}", maskChave(chave));
                
                var response = bacenClient.consultarChave(chave);
                
                if (response.getStatus() == BacenStatus.RATE_LIMITED) {
                    throw new BacenRateLimitException("Rate limit atingido");
                }
                
                if (response.getStatus() == BacenStatus.TEMPORARY_ERROR) {
                    throw new BacenTemporaryUnavailableException("BACEN temporariamente indisponível");
                }
                
                return new PixValidationResult(response.getChave(), response.getTitular());
            },
            "consulta-pix-bacen"
        );
    }
    
    private String maskChave(String chave) {
        if (chave.length() > 6) {
            return chave.substring(0, 3) + "***" + chave.substring(chave.length() - 3);
        }
        return "***";
    }
}

2. TED: Integração com Outros Bancos

@Service
public class TedService {
    
    private final InterbankApiClient interbankClient;
    private final RetryService retryService;
    
    public TedConfirmation processarTed(TedRequest request) {
        
        // Configuração específica para TED
        RetryConfig tedConfig = RetryConfig.builder()
            .maxRetries(5) // TED é crítico, mais tentativas
            .backoffStrategy(new ExponentialBackoffStrategy(1000, 30000, 1.5, true))
            .retryByDefault(false) // Conservador: só retry explícito
            .retryableExceptions(Set.of(
                SocketTimeoutException.class,
                InterbankTimeoutException.class,
                InterbankBusyException.class,
                HttpStatus5xxException.class
            ))
            .nonRetryableExceptions(Set.of(
                ContaInexistenteException.class,
                SaldoInsuficienteException.class,
                TedLimitExceededException.class,
                InvalidAccountException.class
            ))
            .build();
        
        return retryService.executeSmartRetry(
            () -> {
                // Validações pré-envio (não-retryable)
                validateTedRequest(request);
                
                // Envio para banco de destino
                var response = interbankClient.processarTed(
                    request.getBancoDestino(),
                    request.getContaDestino(),
                    request.getValor(),
                    request.getFinalidadeTed()
                );
                
                // Mapeamento de respostas para exceptions apropriadas
                return mapToTedConfirmation(response);
                
            },
            "processar-ted"
        );
    }
    
    private void validateTedRequest(TedRequest request) {
        if (request.getValor().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Valor deve ser positivo");
        }
        
        if (request.getValor().compareTo(new BigDecimal("1000000")) > 0) {
            throw new TedLimitExceededException("Valor excede limite diário");
        }
        
        // Outras validações que não devem ser retried...
    }
}

3. Consulta de Score: Integração SERASA/SPC

@Service
public class ScoreService {
    
    @Async
    @Retryable(
        value = {SocketTimeoutException.class, ServiceUnavailableException.class},
        maxAttempts = 4,
        backoff = @Backoff(
            delay = 2000,      // 2 segundos inicial
            multiplier = 1.8,   // Crescimento moderado
            maxDelay = 20000,   // Máximo 20 segundos
            random = true       // Jitter automático
        )
    )
    public CompletableFuture<ScoreResult> consultarScore(String cpf) {
        
        log.debug("Consultando score para CPF: {}", maskCpf(cpf));
        
        try {
            // Consulta paralela em múltiplos bureaus
            CompletableFuture<ScoreData> serasaFuture = consultarSerasa(cpf);
            CompletableFuture<ScoreData> spcFuture = consultarSpc(cpf);
            
            // Combina resultados (pelo menos 1 deve dar certo)
            return CompletableFuture.allOf(serasaFuture, spcFuture)
                .thenApply(v -> {
                    var serasaData = serasaFuture.getNow(null);
                    var spcData = spcFuture.getNow(null);
                    
                    return combineScoreResults(serasaData, spcData);
                });
                
        } catch (Exception e) {
            log.error("Erro ao consultar score para CPF {}: {}", maskCpf(cpf), e.getMessage());
            throw new ScoreConsultationException("Falha na consulta de score", e);
        }
    }
    
    @Recover
    public CompletableFuture<ScoreResult> recoverScoreConsultation(Exception ex, String cpf) {
        log.error("Todas as tentativas de consulta de score falharam para CPF {}: {}", 
            maskCpf(cpf), ex.getMessage());
            
        // Retorna score padrão/conservador
        var defaultScore = ScoreResult.builder()
            .cpf(cpf)
            .score(500) // Score neutro
            .fonte("DEFAULT")
            .dataConsulta(Instant.now())
            .observacao("Consulta falhou, usando score padrão")
            .build();
            
        return CompletableFuture.completedFuture(defaultScore);
    }
    
    private CompletableFuture<ScoreData> consultarSerasa(String cpf) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return serasaApiClient.getScore(cpf);
            } catch (Exception e) {
                log.warn("Falha na consulta SERASA para CPF {}: {}", maskCpf(cpf), e.getMessage());
                return null;
            }
        });
    }
}

Circuit Breaker + Retry: Proteção Dupla

@Component
public class ResilientBankingService {
    
    private final CircuitBreaker circuitBreaker;
    private final RetryService retryService;
    private final MeterRegistry meterRegistry;
    
    public ResilientBankingService() {
        this.circuitBreaker = CircuitBreaker.ofDefaults("banking-service");
        
        // Configuração do Circuit Breaker
        circuitBreaker.getEventPublisher()
            .onStateTransition(event -> 
                log.info("Circuit breaker mudou de {} para {}", 
                    event.getStateTransition().getFromState(),
                    event.getStateTransition().getToState()))
            .onCallNotPermitted(event -> 
                meterRegistry.counter("circuit.breaker.rejected",
                    "service", "banking").increment())
            .onError(event ->
                meterRegistry.counter("circuit.breaker.error",
                    "service", "banking",
                    "error", event.getThrowable().getClass().getSimpleName()).increment());
    }
    
    public <T> T executeResilient(Supplier<T> operation, String operationName) {
        
        // Primeira camada: Circuit Breaker
        Supplier<T> decoratedSupplier = CircuitBreaker
            .decorateSupplier(circuitBreaker, operation);
        
        // Segunda camada: Retry (só se circuit breaker permitir)
        RetryConfig retryConfig = RetryConfig.builder()
            .maxRetries(3)
            .backoffStrategy(new ExponentialBackoffStrategy(1000, 10000, 2.0, true))
            .retryableExceptions(Set.of(
                SocketTimeoutException.class,
                ConnectException.class
            ))
            .nonRetryableExceptions(Set.of(
                CallNotPermittedException.class // Circuit breaker aberto
            ))
            .build();
        
        try {
            return retryService.executeSmartRetry(decoratedSupplier, operationName, retryConfig);
            
        } catch (CallNotPermittedException e) {
            log.warn("Circuit breaker aberto para operação: {}", operationName);
            
            // Fallback ou cache
            return handleCircuitBreakerOpen(operationName);
        }
    }
    
    @SuppressWarnings("unchecked")
    private <T> T handleCircuitBreakerOpen(String operationName) {
        // Estratégias de fallback por tipo de operação
        
        if (operationName.contains("consulta-score")) {
            return (T) getDefaultScore();
        }
        
        if (operationName.contains("validacao-antifraude")) {
            return (T) getConservativeAntiFraudResult();
        }
        
        if (operationName.contains("cotacao-moeda")) {
            return (T) getCachedExchangeRate();
        }
        
        throw new ServiceUnavailableException("Serviço " + operationName + " temporariamente indisponível");
    }
}

Observabilidade: Métricas de Retry

1. Métricas Essenciais

@Component
public class RetryMetrics {
    
    private final MeterRegistry meterRegistry;
    
    public void recordRetryAttempt(String operation, int attempt, String result, long durationMs) {
        
        // Contador de tentativas por resultado
        meterRegistry.counter("retry.attempts",
            "operation", operation,
            "attempt", String.valueOf(attempt),
            "result", result).increment();
        
        // Histograma de duração por tentativa
        meterRegistry.timer("retry.attempt.duration",
            "operation", operation,
            "attempt", String.valueOf(attempt)).record(durationMs, TimeUnit.MILLISECONDS);
        
        // Gauge do número de operações atualmente em retry
        meterRegistry.gauge("retry.operations.active",
            Tags.of("operation", operation),
            getCurrentActiveRetries(operation));
    }
    
    public void recordFinalResult(String operation, boolean success, int totalAttempts, long totalDurationMs) {
        
        // Taxa de sucesso vs falha final
        meterRegistry.counter("retry.final.result",
            "operation", operation,
            "success", String.valueOf(success)).increment();
        
        // Distribuição de tentativas necessárias para sucesso
        if (success) {
            meterRegistry.counter("retry.success.attempts",
                "operation", operation,
                "attempts", String.valueOf(totalAttempts)).increment();
        }
        
        // Tempo total end-to-end
        meterRegistry.timer("retry.total.duration",
            "operation", operation,
            "result", success ? "success" : "failed").record(totalDurationMs, TimeUnit.MILLISECONDS);
    }
}

2. Dashboard Grafana

-- Queries recomendadas para dashboards

-- Taxa de sucesso por operação (últimas 5 min)
rate(retry_final_result_total{success="true"}[5m]) / 
rate(retry_final_result_total[5m]) * 100

-- Percentil 95 de duração total por operação
histogram_quantile(0.95, 
  rate(retry_total_duration_bucket[5m]))

-- Distribuição de tentativas necessárias para sucesso
sum by (attempts) (
  rate(retry_success_attempts_total[5m])
)

-- Operações que mais fazem retry
topk(10, 
  sum by (operation) (
    rate(retry_attempts_total{result="retry"}[5m])
  )
)

-- Circuit breaker status
circuit_breaker_state{state="OPEN"} > 0

3. Alertas Críticos

# alerts.yml
groups:
  - name: retry-patterns
    rules:
      - alert: HighRetryRate
        expr: |
          sum(rate(retry_attempts_total{result="retry"}[5m])) by (operation) /
          sum(rate(retry_attempts_total[5m])) by (operation) > 0.3
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Taxa de retry alta para {{ $labels.operation }}"
          description: "Operação {{ $labels.operation }} tem {{ $value | humanizePercentage }} de taxa de retry"
      
      - alert: RetryFailureSpike
        expr: |
          sum(rate(retry_final_result_total{success="false"}[5m])) by (operation) > 10
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Spike de falhas após retry para {{ $labels.operation }}"
          description: "{{ $value }} falhas/segundo após esgotar retries"
      
      - alert: CircuitBreakerOpen
        expr: circuit_breaker_state{state="OPEN"} > 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "Circuit breaker aberto para {{ $labels.service }}"
          description: "Serviço {{ $labels.service }} com circuit breaker aberto"

Padrões de Produção Bancária

1. Configuração por Criticidade

@Configuration
public class RetryConfiguration {
    
    @Bean
    @Qualifier("critical")
    public RetryConfig criticalOperationsRetry() {
        // Para operações críticas: PIX, TED, Débitos
        return RetryConfig.builder()
            .maxRetries(5)
            .backoffStrategy(new ExponentialBackoffStrategy(
                500,    // 500ms inicial
                15000,  // máximo 15s
                1.8,    // crescimento moderado
                true    // jitter
            ))
            .retryByDefault(false)
            .retryableExceptions(Set.of(
                SocketTimeoutException.class,
                ConnectException.class,
                TemporaryResourceException.class
            ))
            .build();
    }
    
    @Bean
    @Qualifier("standard")
    public RetryConfig standardOperationsRetry() {
        // Para operações padrão: Consultas, Validações
        return RetryConfig.builder()
            .maxRetries(3)
            .backoffStrategy(new ExponentialBackoffStrategy(
                1000,   // 1s inicial
                10000,  // máximo 10s
                2.0,    // crescimento padrão
                true
            ))
            .retryByDefault(false)
            .build();
    }
    
    @Bean
    @Qualifier("background")
    public RetryConfig backgroundOperationsRetry() {
        // Para operações background: Relatórios, Sincronizações
        return RetryConfig.builder()
            .maxRetries(10)  // Pode tentar mais vezes
            .backoffStrategy(new ExponentialBackoffStrategy(
                2000,   // 2s inicial
                60000,  // máximo 1 minuto
                2.0,
                true
            ))
            .retryByDefault(true)  // Mais permissivo
            .build();
    }
}

2. Integração com Health Checks

@Component
public class RetryHealthIndicator implements HealthIndicator {
    
    private final MeterRegistry meterRegistry;
    
    @Override
    public Health health() {
        var builder = Health.up();
        
        // Verifica se há muitas operações falhando após retry
        var criticalFailures = getCriticalFailureRate();
        if (criticalFailures > 0.05) { // 5%
            builder.down()
                .withDetail("critical_failure_rate", criticalFailures)
                .withDetail("status", "HIGH_RETRY_FAILURE_RATE");
        }
        
        // Verifica se há circuit breakers abertos
        var openCircuitBreakers = getOpenCircuitBreakers();
        if (!openCircuitBreakers.isEmpty()) {
            builder.down()
                .withDetail("open_circuit_breakers", openCircuitBreakers)
                .withDetail("status", "CIRCUIT_BREAKERS_OPEN");
        }
        
        // Verifica latência de retries
        var p99Latency = getP99RetryLatency();
        if (p99Latency > Duration.ofSeconds(30)) {
            builder.down()
                .withDetail("p99_retry_latency_seconds", p99Latency.getSeconds())
                .withDetail("status", "HIGH_RETRY_LATENCY");
        }
        
        return builder
            .withDetail("active_retries", getActiveRetryCount())
            .withDetail("success_rate_5m", getSuccessRate5m())
            .build();
    }
}

3. Análise de Padrões de Falha

@Service
public class RetryAnalyticsService {
    
    @Scheduled(fixedDelay = 300000) // A cada 5 minutos
    public void analyzeRetryPatterns() {
        
        // 1. Detecta operações com alta taxa de retry
        var highRetryOperations = findHighRetryOperations();
        for (var operation : highRetryOperations) {
            alertingService.sendAlert(AlertLevel.WARNING,
                "Alta taxa de retry detectada para operação: " + operation.getName() +
                " (Taxa: " + operation.getRetryRate() + "%)");
        }
        
        // 2. Identifica horários de pico de falhas
        var failurePatterns = analyzeFailureTimePatterns();
        if (failurePatterns.hasSignificantPattern()) {
            alertingService.sendAlert(AlertLevel.INFO,
                "Padrão de falhas identificado: " + failurePatterns.getDescription());
        }
        
        // 3. Sugere otimizações de configuração
        var optimizations = suggestRetryOptimizations();
        if (!optimizations.isEmpty()) {
            log.info("Sugestões de otimização de retry: {}", optimizations);
        }
    }
    
    private List<RetryOptimization> suggestRetryOptimizations() {
        var suggestions = new ArrayList<RetryOptimization>();
        
        // Analisa se alguma operação precisaria de mais/menos retries
        var operations = retryMetricsRepository.getOperationStats();
        
        for (var operation : operations) {
            if (operation.getSuccessRateAfterMaxRetries() > 0.8 && operation.getMaxRetries() < 5) {
                suggestions.add(new RetryOptimization(
                    operation.getName(),
                    "Aumentar maxRetries para " + (operation.getMaxRetries() + 2),
                    "80% das operações teriam sucesso com mais tentativas"
                ));
            }
            
            if (operation.getFirstAttemptSuccessRate() > 0.95 && operation.getMaxRetries() > 3) {
                suggestions.add(new RetryOptimization(
                    operation.getName(),
                    "Reduzir maxRetries para 2",
                    "95% das operações já sucedem na primeira tentativa"
                ));
            }
        }
        
        return suggestions;
    }
}

Boas Práticas para Produção

1. Configuração Diferenciada por Ambiente

# application-prod.yml
retry:
  banking:
    pix:
      max-retries: 5
      base-delay: 500ms
      max-delay: 15s
      multiplier: 1.8
      jitter: true
    
    ted:
      max-retries: 7
      base-delay: 1s
      max-delay: 30s
      multiplier: 1.5
      jitter: true
    
    score-consultation:
      max-retries: 3
      base-delay: 2s
      max-delay: 20s
      multiplier: 2.0
      jitter: true
      fallback-enabled: true

# application-dev.yml  
retry:
  banking:
    pix:
      max-retries: 2  # Falha mais rápido em dev
      base-delay: 100ms
      max-delay: 2s

2. Testes de Resiliência

@ExtendWith(MockitoExtension.class)
class RetryResilienceTest {
    
    @Test
    void shouldRetryTransientFailuresAndEventuallySucceed() {
        // Given
        var mockService = mock(ExternalBankingService.class);
        when(mockService.processPayment(any()))
            .thenThrow(new SocketTimeoutException("Connection timeout"))  // 1ª tentativa
            .thenThrow(new ConnectException("Connection refused"))         // 2ª tentativa
            .thenReturn(new PaymentResult("SUCCESS", "txn-123"));          // 3ª tentativa
        
        var retryService = new SmartRetry(meterRegistry);
        var config = RetryConfig.builder()
            .maxRetries(3)
            .backoffStrategy(new FixedDelayStrategy(10)) // Teste rápido
            .build();
        
        // When
        var result = retryService.executeSmartRetry(
            () -> mockService.processPayment(new PaymentRequest()),
            "test-payment",
            config
        );
        
        // Then
        assertThat(result.getStatus()).isEqualTo("SUCCESS");
        verify(mockService, times(3)).processPayment(any());
    }
    
    @Test
    void shouldNotRetryNonRetryableExceptions() {
        // Given
        var mockService = mock(ExternalBankingService.class);
        when(mockService.processPayment(any()))
            .thenThrow(new IllegalArgumentException("Invalid account"));
        
        // When & Then
        assertThatThrownBy(() -> 
            retryService.executeSmartRetry(
                () -> mockService.processPayment(new PaymentRequest()),
                "test-payment",
                config
            )
        ).isInstanceOf(IllegalArgumentException.class);
        
        verify(mockService, times(1)).processPayment(any()); // Só 1 tentativa
    }
}

Conclusão

Estratégias inteligentes de retry são fundamentais para a resiliência de sistemas bancários. Exponential backoff com jitter evita sobrecarga, while circuit breakers protegem contra falhas em cascata.

A implementação deve considerar a criticidade da operação: PIX e TED precisam de mais persistência, while consultas podem falhar mais rapidamente. Observabilidade detalhada permite otimização contínua das configurações.

Principais benefícios:

  • Resiliência: Operações críticas se recuperam de falhas transitórias
  • Performance: Evita sobrecarga em sistemas já estressados
  • Experiência: Usuários não enfrentam falhas desnecessárias
  • Observabilidade: Visibilidade total sobre padrões de falha
  • Otimização: Ajuste contínuo baseado em dados reais

Próximos passos:

No próximo artigo, exploraremos Event Streaming com Kafka para sistemas bancários, incluindo particionamento por conta, processamento em tempo real de transações e garantias de ordem em operações críticas.