Mastering API Retry Strategies in Java: 8 Proven Techniques
This article walks through eight practical ways to implement retry mechanisms for remote API calls in Java, covering simple loops, recursion, Apache HttpClient settings, Spring Retry, Resilience4j, custom utilities, asynchronous thread‑pool retries, and message‑queue based retries, while highlighting trade‑offs and best‑practice guidelines.
When integrating with third‑party services that may be distributed worldwide, network glitches are inevitable; adding a retry mechanism becomes essential. The following sections detail eight concrete implementations, each with code examples, reasoning, and usage tips.
1. Loop Retry
The most straightforward approach wraps the request in a for loop, retrying until success or a maximum count is reached.
int retryTimes = 3;
for(int i = 0; i < retryTimes; i++){
try{
// request code
break;
} catch(Exception e){
// handle exception
Thread.sleep(1000); // 1‑second delay before next attempt
}
}This method directly controls the retry count and delay, making it easy to understand but lacking sophisticated back‑off policies.
2. Recursive Retry
Recursion offers an alternative by letting the request method call itself until the retry limit is hit.
public void requestWithRetry(int retryTimes){
if(retryTimes <= 0) return;
try{
// request code
} catch(Exception e){
Thread.sleep(1000);
requestWithRetry(retryTimes - 1);
}
}Here retryTimes tracks the remaining attempts; the method returns early when the limit is reached.
3. Built‑in HttpClient Retry
Many HTTP clients embed retry logic. Using Apache HttpClient as an example:
For version 4.5+, configure HttpClients.custom().setRetryHandler(...) with DefaultHttpRequestRetryHandler.
For version 5.x, use setRetryStrategy(...) with DefaultHttpRequestRetryStrategy.
CloseableHttpClient httpClient = HttpClients.custom()
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
.build(); CloseableHttpClient httpClient = HttpClients.custom()
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, NEG_ONE_SECOND))
.build();Both snippets set a maximum of three retries; the handler/strategy decides when to retry based on exception type or status code.
4. Spring Retry Library
Spring Retry provides both programmatic and annotation‑driven ways.
Explicit RetryTemplate usage
RetryTemplate retryTemplate = new RetryTemplate();
RetryPolicy retryPolicy = new SimpleRetryPolicy(3);
retryTemplate.setRetryPolicy(retryPolicy);
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(1000);
retryTemplate.setBackOffPolicy(backOffPolicy);
retryTemplate.execute(context -> {
// request code
return null;
});The template applies a simple three‑attempt policy with a fixed 1‑second back‑off.
Annotation‑driven retry
@Retryable(value = Exception.class, maxAttempts = 3)
public void request(){
// request code
}When placed on a public method, Spring creates a proxy that automatically retries on the specified exceptions.
5. Resilience4j
Resilience4j is a lightweight fault‑tolerance library offering retry, circuit‑breaker, and rate‑limiter features.
Programmatic configuration
RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(1000))
.retryOnResult(response -> response.getStatus() == 500)
.retryOnException(e -> e instanceof WebServiceException)
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
.failAfterMaxAttempts(true)
.build();
Retry retry = retryRegistry.retry("name", config);
CheckedFunction0<String> retryableSupplier = Retry.decorateCheckedSupplier(
retry, () -> {
// request code
return "result";
});Annotation usage (Spring Boot)
@Retryable(value = MyException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void doSomething(){
// request code
}6. Custom Retry Utility
A lightweight self‑contained retry helper can be built by defining a Callback abstract class, a RetryResult wrapper, and an RetryExecutor that loops until success.
public abstract class Callback {
public abstract RetryResult doProcess();
}
public class RetryResult {
private Boolean isRetry;
private Object obj;
public static RetryResult ofResult(Boolean isRetry, Object obj){
return new RetryResult(isRetry, obj);
}
public static RetryResult ofResult(Boolean isRetry){
return new RetryResult(isRetry, null);
}
}
public class RetryExecutor {
public static Object execute(int retryCount, Callback callback){
for(int cur = 0; cur < retryCount; cur++){
RetryResult result = callback.doProcess();
if(result.isRetry()) continue;
return result.getObj();
}
return null;
}
}Usage example:
int maxRetryCount = 3;
Object result = RetryExecutor.execute(maxRetryCount, new Callback(){
@Override
public RetryResult doProcess(){
// request logic
// return RetryResult.ofResult(true) to retry
// or RetryResult.ofResult(false, result) to finish
return RetryResult.ofResult(false, "ok");
}
});7. Asynchronous Retry with ThreadPoolExecutor
For latency‑sensitive scenarios, submit the request as a Callable to a thread pool and retry based on the Future outcome.
int maxRetryTimes = 3;
int currentRetryTimes = 0;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>()
);
Callable<String> task = () -> {
// request code
return "result";
};
Future<String> future;
while(currentRetryTimes < maxRetryTimes){
try{
future = executor.submit(task);
String result = future.get();
break; // success
} catch(Exception e){
currentRetryTimes++;
Thread.sleep(1000);
}
}The loop retries up to three times, pausing a second between attempts.
8. Message‑Queue Based Retry
To guarantee durability across service outages, failed requests can be re‑queued into a message broker such as RocketMQ.
@Component
@RocketMQMessageListener(topic = "myTopic", consumerGroup = "myConsumerGroup")
public class MyConsumer implements RocketMQListener<String>{
@Override
public void onMessage(String message){
try{
// request code
} catch(Exception e){
DefaultMQProducer producer = new DefaultMQProducer("myProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
try{
producer.start();
Message msg = new Message("myTopic", "myTag", message.getBytes());
producer.send(msg);
} catch(Exception ex){
// handle send failure
} finally {
producer.shutdown();
}
}
}
}The consumer catches exceptions, creates a producer, and republishes the payload for later processing, ensuring no loss even if the original service is down.
Best Practices & Pitfalls
Set a reasonable retry count and interval to avoid overwhelming the downstream service.
Ensure idempotency for write operations; otherwise retries may cause duplicate effects.
Guard against concurrent retries causing duplicate requests; consider locks or distributed locks.
Distinguish retry‑able exceptions (e.g., timeouts) from non‑retry‑able ones (e.g., validation errors).
Avoid infinite loops; always enforce a hard limit on attempts.
References
[1] "Java业务开发常见错误100例"
[2] https://juejin.cn/post/7028947828248412168#heading-9
[3] https://resilience4j.readme.io/docs/retry
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
