Backend Development 17 min read

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.

macrozheng
macrozheng
macrozheng
Mastering Retry Strategies in Java: 8 Proven Methods for Reliable API Calls

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

for

loop 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>&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.retry&lt;/groupId&gt;
    &lt;artifactId&gt;spring-retry&lt;/artifactId&gt;
    &lt;version&gt;1.3.1&lt;/version&gt;
&lt;/dependency&gt;
</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>&lt;dependency&gt;
    &lt;groupId&gt;io.github.resilience4j&lt;/groupId&gt;
    &lt;artifactId&gt;resilience4j-spring-boot2&lt;/artifactId&gt;
    &lt;version&gt;1.7.0&lt;/version&gt;
&lt;/dependency&gt;
</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

Callback

abstract class, a

RetryResult

holder, and an

RetryExecutor

that 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

Callable

to 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.

JavaconcurrencySpringretryHttpClientResilience4j
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.