Mastering Retry Strategies in Java: 8 Practical Implementations
This article explains why retry mechanisms are essential for unreliable network calls in distributed systems and presents eight concrete Java implementations—including loop, recursion, HttpClient built‑in, Spring Retry, Resilience4j, custom utilities, asynchronous thread‑pool, and message‑queue approaches—plus best‑practice guidelines to avoid common pitfalls.
Retry Mechanism Implementations
1. Loop Retry
The simplest method wraps the request in a
forloop, retrying up to a configured maximum and sleeping between attempts.
<code>int retryTimes = 3;
for (int i = 0; i < retryTimes; i++) {
try {
// request code
break;
} catch (Exception e) {
// handle exception
Thread.sleep(1000); // delay 1s before next try
}
}
</code>2. Recursive Retry
A recursive method calls itself when an exception occurs, decreasing the remaining retry count each time.
<code>public void requestWithRetry(int retryTimes) {
if (retryTimes <= 0) return;
try {
// request code
} catch (Exception e) {
Thread.sleep(1000);
requestWithRetry(retryTimes - 1);
}
}
</code>3. Built‑in HttpClient Retry
Apache HttpClient provides built‑in retry handlers. For version 4.5+, use
HttpClients.custom().setRetryHandler(...); for 5.x, use
setRetryStrategy(...).
<code>CloseableHttpClient httpClient = HttpClients.custom()
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
.build();
</code>4. Spring Retry Library
Spring Retry offers
RetryTemplatefor programmatic retries and
@Retryableannotations for declarative retries.
<code>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;
});
</code>Or with annotations:
<code>@Retryable(value = Exception.class, maxAttempts = 3)
public void request() {
// request code
}
</code>5. Resilience4j
Resilience4j provides a lightweight retry module configurable via
RetryConfigand usable through decorators or annotations.
<code>RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(1000))
.retryOnResult(r -> r.getStatus() == 500)
.retryOnException(e -> e instanceof WebServiceException)
.build();
Retry retry = retryRegistry.retry("myRetry", config);
CheckedFunction0<String> retryableSupplier = Retry.decorateCheckedSupplier(
retry, () -> "result");
</code>6. Custom Retry Utility
A lightweight custom utility can be built with a
Callbackabstract class returning a
RetryResult. The executor loops until the callback signals success or the maximum attempts are reached.
<code>public abstract class Callback {
public abstract RetryResult doProcess();
}
public class RetryResult {
private Boolean isRetry;
private Object obj;
// constructors, getters omitted
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 i = 0; i < retryCount; i++) {
RetryResult result = callback.doProcess();
if (result.isRetry()) continue;
return result.getObj();
}
return null;
}
}
</code>7. Asynchronous Retry with ThreadPoolExecutor
Using a thread pool, the request is submitted as a
Callable. If the task fails, the loop retries after a delay.
<code>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;
} catch (Exception e) {
currentRetryTimes++;
Thread.sleep(1000);
}
}
</code>8. Message‑Queue Retry (RocketMQ)
When a request fails, the message can be re‑published to RocketMQ for later processing, ensuring reliability even if the service is temporarily unavailable.
<code>@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);
} finally {
producer.shutdown();
}
}
}
}
</code>Best Practices and Cautions
Set reasonable retry counts and intervals to avoid overwhelming the target service.
Ensure the operation is idempotent; otherwise, repeated attempts may cause inconsistent state.
Handle concurrency carefully—use locks or distributed locks when multiple threads may retry the same request.
Distinguish retry‑able exceptions (e.g., timeouts, connection errors) from non‑retry‑able ones (e.g., validation errors).
Avoid infinite loops; always enforce a maximum number of attempts and back‑off strategy.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.