Mastering Retry Strategies in Java: 8 Proven Methods for Reliable API Calls
This article explains why retry mechanisms are essential for distributed Java applications and walks through eight practical implementations—including loop, recursion, Apache HttpClient, Spring Retry, Resilience4j, custom utilities, asynchronous thread‑pool retries, and message‑queue based retries—plus best‑practice guidelines to avoid common pitfalls.
Retry Mechanism Implementation
In distributed systems, third‑party services may be unreliable, so adding a retry mechanism is essential. This article presents eight practical ways to implement retries in Java.
1. Loop Retry
Wrap the request in a
forloop and break on success; use
Thread.sleep()to delay 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);
}
}
</code>2. Recursive Retry
Define a method that calls itself until the maximum retry count is reached.
<code>public void requestWithRetry(int retryTimes) {
if (retryTimes <= 0) return;
try {
// request code
} catch (Exception e) {
Thread.sleep(1000);
requestWithRetry(retryTimes - 1);
}
}
</code>3. HttpClient Built‑in Retry
Configure Apache HttpClient (4.5+ or 5.x) with
HttpClients.custom().setRetryHandler(...)or
setRetryStrategy(...)so the client handles retries automatically.
<code>CloseableHttpClient httpClient = HttpClients.custom()
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
.build();
</code> <code>CloseableHttpClient httpClient = HttpClients.custom()
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, NEG_ONE_SECOND))
.build();
</code>4. Spring Retry Library
Spring Retry provides annotations and templates for declarative or programmatic retries.
<code><dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1</version>
</dependency>
</code>Using
RetryTemplate:
<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>Using annotations:
<code>@Retryable(value = Exception.class, maxAttempts = 3)
public void request() {
// request code
}
</code>5. Resilience4j
Resilience4j offers a lightweight retry module that can be configured programmatically or via annotations.
<code><dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.0</version>
</dependency>
</code>Programmatic example:
<code>RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(1000))
.retryOnResult(response -> response.getStatus() == 500)
.retryOnException(e -> e instanceof WebServiceException)
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
.failAfterMaxAttempts(true)
.build();
Retry retry = retryRegistry.retry("name", config);
CheckedFunction0<String> retryableSupplier = Retry.decorateCheckedSupplier(
retry, () -> "result");
</code>Annotation example:
<code>@Service
public class MyService {
@Retryable(value = MyException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void doSomething() {
// request code
}
}
</code>6. Custom Retry Utility
A lightweight custom utility can be built with a
Callbackabstract class, a
RetryResultholder, and an
RetryExecutorthat loops until success or the maximum attempts are exhausted.
<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>Usage example:
<code>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");
}
});
</code>7. Asynchronous Retry with ThreadPoolExecutor
Submit the request as a
Callableto a thread pool and retry the task when a failure occurs.
<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 Based Retry (RocketMQ)
When a request fails, re‑publish the payload to a RocketMQ topic so a consumer can retry later, ensuring durability across service outages.
<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);
} catch (Exception ex) {
// handle send failure
} finally {
producer.shutdown();
}
}
}
}
</code>Best Practices and Precautions
Set reasonable retry counts and intervals to avoid overwhelming the target service.
Ensure the operation is idempotent; otherwise, guard against duplicate writes.
Handle concurrency properly—use locks or distributed locks when multiple threads may retry the same request.
Distinguish retry‑able exceptions (e.g., timeouts) from non‑retryable ones (e.g., validation errors).
Avoid infinite loops; enforce a hard limit on attempts and fallback strategies.
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.