Fallback e Degradação resiliente em APIs com Redis e Circuit Breaker
Parte 1 da série "Building Resilient Java Applications"
🎯 O Problema
Você já passou por isso?
- Sua API depende de serviços externos que falham aleatoriamente
- Quando a API de terceiros cai, seu sistema todo quebra
- Rate limits são atingidos e usuários veem telas de erro
- Timeouts deixam requisições travadas por segundos
Resultado: Usuários frustrados, SLA comprometido, On-call alerts às 3h da manhã.
💡 A Solução
Neste artigo, vamos construir um Weather Dashboard que demonstra como implementar:
✅ Degradação Resiliente - Sistema continua funcionando mesmo quando tudo dá errado
✅ Fallback Chain - Múltiplas camadas de redundância
✅ Circuit Breaker - Previne cascata de falhas
✅ Cache Inteligente - Redis como camada de resiliência
O que você vai aprender
- Como implementar fallback chain completo
- Configurar Circuit Breaker, Retry e Time Limiter (Resilience4j)
- Usar Redis não apenas como cache, mas como salvação em cenários de desastre
- Quando retornar dados stale vs erro gracioso
📚 Conceitos
Degradação Resiliente
É a capacidade do sistema continuar operando em modo reduzido quando componentes falham, ao invés de quebrar completamente.
Exemplo do mundo real: Quando o ar-condicionado de um data center falha, os servidores continuam rodando em temperatura elevada (degradado) ao invés de desligar tudo imediatamente.
Cache Stale
Dados expirados mas ainda válidos. No nosso caso, preferimos retornar temperatura de 10 minutos atrás do que mostrar erro ao usuário.
Circuit Breaker Pattern
Inspirado em disjuntores elétricos. Quando muitas falhas acontecem:
- CLOSED → Operação normal
- OPEN → Para de tentar (evita sobrecarga)
- HALF-OPEN → Testa se voltou ao normal
Fallback Chain
Sequência de alternativas tentadas em ordem:
API Primária → API Secundária → Cache Stale → Erro Gracioso
🏗️ Arquitetura da Solução
Diagrama 1: Componentes do Sistema
Componentes:
- WeatherController: REST API endpoint
- WeatherService: Orquestração + fallback logic
- CacheService: Operações no Redis (get/save/stale)
- Circuit Breakers: Protegem APIs primária e secundária
- Retry Policy: 2 tentativas, 500ms intervalo
- Time Limiter: Timeout 3 segundos
- Redis: Cache TTL 5 minutos
- Linhas tracejadas: Caminhos de fallback
Diagrama 2: Fluxo de Decisão
5 Cenários:
- ✅ Cache Hit: Dados frescos (< 5min) | ~5ms
- ✅ Primary Success: Cache miss → API primária | ~100ms
- ✅ Secondary Success: Primary falha → fallback | ~150ms
- ⚠️ Stale Cache: Ambas APIs falham → cache expirado | ~8ms
- ❌ Graceful Error: Tudo falha → erro amigável | ~2ms
🛠️ Hands-on Lab
Setup do Projeto
Tech Stack:
- Java 21
- Spring Boot 3.2.1
- Redis 7
- Resilience4j 2.2.0
- Docker & Docker Compose
Estrutura:
my-labs-redis-cache/
├── src/main/java/com/mylabs/resilientcache/
│ ├── controller/ # REST endpoints
│ ├── service/ # Lógica de fallback
│ ├── client/ # APIs externas
│ ├── config/ # Configurações
│ └── model/ # DTOs
├── docker-compose.yml
└── Makefile
Clone e execute:
git clone https://github.com/theoscaargomes/my-labs-redis-cache
cd my-labs-redis-cache
make up
1. Configuração do Resilience4j
application.yml:
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10 # Janela de 10 chamadas
permittedNumberOfCallsInHalfOpenState: 3 # 3 chamadas em half-open
waitDurationInOpenState: 10s # Aguarda 10s antes de half-open
failureRateThreshold: 50 # Abre se 50% falharem
slowCallDurationThreshold: 2s # Chamadas > 2s = lentas
instances:
primaryWeatherApi:
baseConfig: default
secondaryWeatherApi:
baseConfig: default
retry:
configs:
default:
maxAttempts: 2 # Máximo 2 tentativas
waitDuration: 500ms # Aguarda 500ms entre tentativas
instances:
primaryWeatherApi:
baseConfig: default
secondaryWeatherApi:
baseConfig: default
timelimiter:
configs:
default:
timeoutDuration: 3s # Timeout de 3 segundos
cancelRunningFuture: true
instances:
primaryWeatherApi:
baseConfig: default
secondaryWeatherApi:
baseConfig: default
O que está acontecendo:
- Circuit Breaker abre após 50% de falhas em janela de 10 chamadas
- Retry tenta 2x com 500ms de intervalo
- Time Limiter aborta chamadas que excedem 3s
2. CacheService - Gestão Inteligente de Cache
@Service
@RequiredArgsConstructor
public class CacheService {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private static final Duration DEFAULT_TTL = Duration.ofMinutes(5);
/**
* Busca dados frescos (dentro do TTL)
*/
public Optional<WeatherResponse> get(String city) {
String key = "weather:" + city.toLowerCase();
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
WeatherResponse data = objectMapper.readValue(json, WeatherResponse.class);
data.setFromCache(true);
data.setStaleData(false);
log.info("[CACHE HIT] Fresh data for city: {}", city);
return Optional.of(data);
}
log.info("[CACHE MISS] No fresh data for city: {}", city);
return Optional.empty();
}
/**
* Busca dados stale (expirados) - último recurso
*/
public Optional<WeatherResponse> getStale(String city) {
String key = "weather:" + city.toLowerCase();
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
WeatherResponse data = objectMapper.readValue(json, WeatherResponse.class);
data.setFromCache(true);
data.setStaleData(true);
log.warn("[CACHE STALE] Returning expired data for city: {}", city);
return Optional.of(data);
}
return Optional.empty();
}
public void save(String city, WeatherResponse data) {
String key = "weather:" + city.toLowerCase();
String json = objectMapper.writeValueAsString(data);
redisTemplate.opsForValue().set(key, json, DEFAULT_TTL);
log.info("[CACHE] Saved data for city: {} (TTL: {})", city, DEFAULT_TTL);
}
}
Ponto-chave: Separamos get() (dados frescos) de getStale() (dados expirados). Isso permite usar cache stale apenas quando todas as APIs falham.
3. WeatherService - Orquestração da Fallback Chain
@Service
@RequiredArgsConstructor
public class WeatherService {
private final OpenWeatherClient primaryClient;
private final WeatherApiClient secondaryClient;
private final CacheService cacheService;
public WeatherResponse getWeather(String city) {
log.info("=== Starting weather request for city: {} ===", city);
// 1. Tenta cache fresco primeiro
return cacheService.get(city)
.map(cachedData -> {
log.info("[SUCCESS] ✓ Cache hit (fresh data) - ~5ms");
return cachedData;
})
.orElseGet(() -> {
// 2. Cache miss - tenta APIs com fallback
WeatherResponse response = fetchWithFallback(city);
// Salva no cache se obteve dados válidos
if (response != null && !response.getDescription().startsWith("Error")) {
cacheService.save(city, response);
}
return response;
});
}
private WeatherResponse fetchWithFallback(String city) {
try {
// 2. Tenta API primária
return callPrimaryApi(city);
} catch (Exception e1) {
log.warn("[FALLBACK] Primary API failed, trying secondary API...");
try {
// 3. Tenta API secundária
return callSecondaryApi(city);
} catch (Exception e2) {
log.warn("[FALLBACK] Secondary API failed, trying stale cache...");
// 4. Último recurso: cache stale
return cacheService.getStale(city)
.map(staleData -> {
log.warn("[DEGRADED] ⚠ Using stale cache data");
return staleData;
})
.orElseGet(() -> {
// 5. Erro gracioso
log.error("[ERROR] ✗ All sources failed");
return WeatherResponse.createError(city,
"All weather services are currently unavailable. Please try again later.");
});
}
}
}
@CircuitBreaker(name = "primaryWeatherApi", fallbackMethod = "primaryApiFallback")
@Retry(name = "primaryWeatherApi")
@TimeLimiter(name = "primaryWeatherApi")
public CompletableFuture<WeatherResponse> callPrimaryApi(String city) {
return CompletableFuture.supplyAsync(() -> {
try {
WeatherResponse response = primaryClient.getWeather(city);
log.info("[SUCCESS] ✓ Primary API - ~100ms");
return response;
} catch (Exception e) {
throw new RuntimeException("Primary API failed", e);
}
});
}
@CircuitBreaker(name = "secondaryWeatherApi", fallbackMethod = "secondaryApiFallback")
@Retry(name = "secondaryWeatherApi")
@TimeLimiter(name = "secondaryWeatherApi")
public CompletableFuture<WeatherResponse> callSecondaryApi(String city) {
return CompletableFuture.supplyAsync(() -> {
try {
WeatherResponse response = secondaryClient.getWeather(city);
log.info("[SUCCESS] ✓ Secondary API (fallback) - ~150ms");
return response;
} catch (Exception e) {
throw new RuntimeException("Secondary API failed", e);
}
});
}
}
Arquitetura da Fallback Chain:
- Cache fresco (melhor caso)
- API primária com Circuit Breaker + Retry + Timeout
- API secundária com Circuit Breaker + Retry + Timeout
- Cache stale (degradação aceitável)
- Erro gracioso (último recurso, mas não quebra)
4. Controller - Endpoints
@RestController
@RequestMapping("/api/weather")
@RequiredArgsConstructor
public class WeatherController {
private final WeatherService weatherService;
private final CircuitBreakerRegistry circuitBreakerRegistry;
@GetMapping("/{city}")
public ResponseEntity<WeatherResponse> getWeather(@PathVariable String city) {
WeatherResponse response = weatherService.getWeather(city);
return ResponseEntity.ok(response);
}
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
Map<String, Object> health = new HashMap<>();
var primaryCB = circuitBreakerRegistry.circuitBreaker("primaryWeatherApi");
health.put("primaryApi", Map.of(
"state", primaryCB.getState().toString(),
"failureRate", primaryCB.getMetrics().getFailureRate() + "%"
));
var secondaryCB = circuitBreakerRegistry.circuitBreaker("secondaryWeatherApi");
health.put("secondaryApi", Map.of(
"state", secondaryCB.getState().toString(),
"failureRate", secondaryCB.getMetrics().getFailureRate() + "%"
));
return ResponseEntity.ok(health);
}
}
📊 Demonstração dos Cenários
Cenário 1: Happy Path (Cache Hit)
Request:
# Primeira chamada
curl http://localhost:8080/api/weather/London
Logs:
=== Starting weather request for city: London ===
[CACHE MISS] No fresh data for city: London
[PRIMARY API] Calling OpenWeather for city: London
[SUCCESS] ✓ Primary API - ~100ms
[CACHE] Saved data for city: London (TTL: 5min)
Response:
{
"city": "London",
"temperature": 18.5,
"description": "Partly Cloudy",
"humidity": 72,
"source": "OpenWeather",
"fromCache": false,
"staleData": false
}
Segunda chamada (cache hit):
[CACHE HIT] Fresh data for city: London
[SUCCESS] ✓ Cache hit (fresh data) - ~5ms
Latência: 100ms → 5ms (95% de redução!)
Cenário 2: Primary Falha → Fallback Secondary
Simula falha da API primária:
# Configura 100% de falha na primary
export PRIMARY_FAIL_RATE=1.0
curl http://localhost:8080/api/weather/Paris
Logs:
[PRIMARY API] Calling OpenWeather for city: Paris
[CIRCUIT BREAKER] Primary API failed: OpenWeather API unavailable
[FALLBACK] Primary API failed, trying secondary API...
[SECONDARY API] Calling WeatherAPI for city: Paris
[SUCCESS] ✓ Secondary API (fallback) - ~150ms
Response:
{
"city": "Paris",
"temperature": 16.2,
"source": "WeatherAPI",
"fromCache": false
}
Sistema continua funcionando! ✅
Cenário 3: Ambas APIs Falham → Cache Stale
Primeiro, popula cache:
curl http://localhost:8080/api/weather/Berlin
# Aguarda 10 minutos (cache expira)
Depois, simula falha de ambas APIs:
export PRIMARY_FAIL_RATE=1.0
export SECONDARY_FAIL_RATE=1.0
curl http://localhost:8080/api/weather/Berlin
Logs:
[CIRCUIT BREAKER] Primary API circuit breaker activated
[FALLBACK] Primary API failed, trying secondary API...
[CIRCUIT BREAKER] Secondary API circuit breaker activated
[FALLBACK] Secondary API failed, trying stale cache...
[CACHE STALE] Returning expired data for city: Berlin
[DEGRADED] ⚠ Using stale cache data
Response:
{
"city": "Berlin",
"temperature": 14.8,
"source": "OpenWeather",
"fromCache": true,
"staleData": true
}
Dados de 10min atrás, mas melhor que erro! ⚠️
Cenário 4: Tudo Falha → Erro Gracioso
Nova cidade sem cache:
curl http://localhost:8080/api/weather/Tokyo
Logs:
[CACHE MISS] No fresh data for city: Tokyo
[CIRCUIT BREAKER] Primary API circuit breaker activated
[FALLBACK] Secondary API failed, trying stale cache...
[CACHE EMPTY] No stale data available for city: Tokyo
[ERROR] ✗ All sources failed, returning graceful error
Response:
{
"city": "Tokyo",
"description": "Error: All weather services are currently unavailable. Please try again later.",
"timestamp": "2026-01-29T02:30:00Z",
"fromCache": false
}
HTTP 200 com mensagem amigável, não HTTP 500! ✅
Cenário 5: Circuit Breaker em Ação
Depois de várias falhas, o Circuit Breaker abre e para de tentar:
Health check:
curl http://localhost:8080/api/weather/health
Response:
{
"primaryApi": {
"state": "OPEN",
"failureRate": "100.0%"
},
"secondaryApi": {
"state": "CLOSED",
"failureRate": "0.0%"
}
}
Circuit aberto = pula direto para fallback (evita sobrecarga)
📈 Resultados
Latência por Cenário
| Cenário | Latência | Status |
|---|---|---|
| Cache Hit | ~5ms | ✅ Ideal |
| Primary API | ~100ms | ✅ Normal |
| Secondary API | ~150ms | ✅ Degraded |
| Cache Stale | ~8ms | ⚠️ Degraded |
| Graceful Error | ~2ms | ❌ Error |
Disponibilidade
Sem resiliência:
Primary API availability: 70%
System availability: 70%
Com fallback chain:
Primary API: 70%
Secondary API: 80%
Cache: 99.9%
System availability: 99.9%+
Melhoria: De 70% para 99.9% de disponibilidade!
🎯 Trade-offs
Vantagens
✅ Alta disponibilidade - Sistema raramente fica indisponível
✅ Performance - Cache reduz latência drasticamente
✅ Custo - Menos chamadas para APIs pagas
✅ UX - Usuário sempre recebe alguma resposta
Desvantagens
⚠️ Complexidade - Mais código para manter
⚠️ Stale data - Dados podem estar desatualizados
⚠️ Debug - Mais difícil rastrear problemas
⚠️ Infra - Dependência adicional (Redis)
Quando Usar
✅ Dados que mudam com frequência moderada (clima, cotações)
✅ APIs externas instáveis ou com rate limit
✅ Alta disponibilidade é crítica
✅ Tolerância a dados levemente desatualizados
Quando NÃO Usar
❌ Dados críticos (transações financeiras)
❌ APIs muito estáveis e rápidas
❌ Dados que mudam em tempo real
❌ Sistemas simples sem necessidade de HA
🎓 Conclusão
Neste artigo, construímos um sistema resiliente que:
- Nunca quebra - Sempre retorna algo útil ao usuário
- Degrada mas com resiliencia - Prioriza dados frescos, mas aceita stale quando necessário
- Protege APIs externas - Circuit Breaker previne sobrecarga
- Melhora performance - Cache reduz latência em 95%
Principais aprendizados:
- Cache não é apenas performance, é resiliência
- Dados stale > erro para usuário
- Circuit Breaker economiza recursos e acelera fallback
- Sempre tenha um plano B, C e D
Próximo Artigo
Integration Testing with Testcontainers - Como testar toda essa resiliência de forma automatizada, com Redis e APIs mockadas rodando em containers.
💻 Source Code
Código completo disponível no GitHub:
📦 github.com/theoscaargomes/my-labs-redis-cache
Para rodar localmente:
git clone https://github.com/theoscaargomes/my-labs-redis-cache
cd my-labs-redis-cache
make up
make test-happy
Gostou? Deixe um ⭐ no repo e compartilhe com sua equipe!
Dúvidas? Comenta aqui embaixo que eu respondo.
Autor: Oscar Gomes
Série: Building Resilient Java Applications
Artigo: #1 - Redis as Resilience Layer


Top comments (0)