Circuit Breakers & Retries (Resilience4j)
Prevent cascading failures with fail-fast circuit breakers and safe retry policies.
Problem it solves
- Dependency failures can cascade and exhaust thread pools if every request keeps waiting/retrying blindly.
- Transient and sustained failures need different handling strategies.
- Resilience4j helps you fail fast, retry safely, and recover predictably with explicit policies.
Definition
Resilience4j is a lightweight fault-tolerance library for Java/Spring Boot. It provides CircuitBreaker, Retry, TimeLimiter, Bulkhead, and RateLimiter modules, with annotations or programmatic composition.
Why It Matters
- Stops dependency outages from exhausting request threads.
- Improves recovery speed with controlled retries and half-open probes.
- Provides production metrics/events for resilience tuning.
Code Variations
application.yml baseline configyamlresilience4j:
retry:
instances:
paymentRetry:
maxAttempts: 3
waitDuration: 200ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
circuitbreaker:
instances:
paymentCb:
slidingWindowType: COUNT_BASED
slidingWindowSize: 20
minimumNumberOfCalls: 10
failureRateThreshold: 50
slowCallRateThreshold: 60
slowCallDurationThreshold: 2s
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 5@Service
class PaymentGatewayClient {
@Retry(name = "paymentRetry", fallbackMethod = "fallback")
@CircuitBreaker(name = "paymentCb", fallbackMethod = "fallback")
public PaymentResponse charge(PaymentRequest req) {
return httpClient.charge(req);
}
public PaymentResponse fallback(PaymentRequest req, Throwable t) {
return PaymentResponse.accepted("queued_for_retry");
}
}public PaymentResponse charge(PaymentRequest req) {
String key = req.idempotencyKey();
if (processedRepo.existsById(key)) return processedRepo.get(key);
PaymentResponse r = gateway.charge(req); // retriable only if safe
processedRepo.save(key, r);
return r;
}Retry vs Circuit Breaker in practice
| Aspect | Retry | Circuit Breaker |
|---|---|---|
| Primary goal | Recover transient failures | Fail fast during sustained failures |
| When triggered | On selected exceptions/statuses | When failure/slow-call thresholds exceed limits |
| Key knobs | Attempts, wait duration, backoff+jitter | Sliding window, failure rate, wait in OPEN, half-open calls |
| Biggest risk | Retry storm under outage | Too aggressive opening causing false fail-fast |
| Must-have pairing | Idempotency + timeout | Fallback + observability + timeout |
Common Confusions Cleared Up
If dependency is hard-down, retries just add pressure. Breaker should open quickly to protect your service and downstream.
Allow limited calls in HALF_OPEN to test dependency health before closing circuit.
High latency can cause thread-pool starvation even with low error rate; track both slow calls and failures.
Variations / Implementations
Recommended call chain
Timeout first, then retry (for retriable failures), then circuit breaker for sustained faults. Keep retries bounded and only for idempotent operations.
Annotation-driven Spring style
Use @CircuitBreaker, @Retry, @TimeLimiter with named configs and fallback methods for readable policy management.
Programmatic decorators
Use Resilience4j decorators when you need explicit policy composition and runtime control in non-Spring layers.
Common Mistakes / Anti-Patterns
- Retrying all exceptions blindly (including validation/auth errors).
- No timeout configured, causing long hangs.
- Fallback returning stale/incorrect data without signaling degraded mode.
- Same breaker config for all dependencies despite different latency/error profiles.
- No monitoring of OPEN/HALF_OPEN transitions.