Step 43 of 51
Retry strategies: exponential backoff, jitter, max attempts. When to retry vs when to fail fast.
Retry — ลองใหม่เมื่อ fail ชั่วขณะ
Network calls fail transiently: timeouts, temporary DNS issues, overloaded servers. Retrying with backoff gives the system time to recover. Not all errors should be retried — business logic errors (404, validation) will fail again.
| Retry | Fail Fast |
|---|---|
| Connection timeout | 4xx client errors |
| 5xx server errors (transient) | Business validation errors |
| Socket timeout | Not found (404) |
| Rate limited (429 with Retry-After) | Unauthorized (401) |
resilience4j:
retry:
configs:
default:
max-attempts: 3
wait-duration: 1s
retry-exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- org.springframework.web.client.HttpServerErrorException
ignore-exceptions:
- com.example.BusinessException
- com.example.ValidationException
instances:
productService:
base-config: default
max-attempts: 3
wait-duration: 500ms
paymentService:
max-attempts: 5
wait-duration: 1s
@Service
@RequiredArgsConstructor
public class ExternalApiService {
private final RestClient restClient;
@Retry(name = "productService", fallbackMethod = "productFallback")
public ProductResponse getProduct(Long id) {
return restClient.get()
.uri("/api/products/{id}", id)
.retrieve()
.body(ProductResponse.class);
}
@Retry(name = "paymentService", fallbackMethod = "paymentFallback")
public PaymentResult processPayment(PaymentRequest request) {
return restClient.post()
.uri("/api/payments")
.body(request)
.retrieve()
.body(PaymentResult.class);
}
private ProductResponse productFallback(Long id, Throwable t) {
log.warn("Product service unavailable after retries: {}",
t.getMessage());
return new ProductResponse(id, "Temporarily Unavailable",
BigDecimal.ZERO, 0);
}
private PaymentResult paymentFallback(
PaymentRequest request, Throwable t) {
log.error("Payment processing failed after retries");
return PaymentResult.failed(request.transactionId(),
"Payment service unavailable. Please try again later.");
}
}
The wait-duration is the base. Resilience4j uses exponential backoff by default: each retry waits longer. Add jitter to prevent the thundering herd problem (all clients retrying simultaneously):
@Configuration
public class RetryConfig {
@Bean
public RetryCustomizer retryCustomizer() {
return (id, builder) -> {
IntervalFunction intervalWithJitter =
IntervalFunction.ofExponentialBackoff(
Duration.ofSeconds(1), 2.0,
Duration.ofSeconds(30));
builder.waitDuration(Duration.ofSeconds(1))
.intervalFunction(intervalWithJitter);
return builder;
};
}
}
Backoff sequence with jitter: ~1s, ~2s, ~4s (capped at 30s).
Combine retry and circuit breaker. Retries handle transient failures; the circuit breaker handles sustained failures:
@CircuitBreaker(name = "productService")
@Retry(name = "productService", fallbackMethod = "productFallback")
public ProductResponse getProduct(Long id) {
return restClient.get()
.uri("/api/products/{id}", id)
.retrieve()
.body(ProductResponse.class);
}
Execution order: Circuit Breaker wraps Retry. If retries exhaust, the circuit breaker counts it as a failure. After enough failures, the circuit opens.
@Configuration
@RequiredArgsConstructor
@Slf4j
public class RetryEventConfig {
@PostConstruct
public void registerEventConsumers() {
var registry = RetryRegistry.ofDefaults();
registry.getEventPublisher()
.onRetry(event -> log.warn(
"Retry #{} for '{}': {}",
event.getNumberOfRetryAttempts(),
event.getName(),
event.getLastThrowable().getMessage()))
.onError(event -> log.error(
"All retries exhausted for '{}'",
event.getName()))
.onSuccess(event -> log.debug(
"Call succeeded for '{}' after {} attempts",
event.getName(),
event.getNumberOfRetryAttempts()));
}
}