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.

int retryTimes = 3;
for (int i = 0; i < retryTimes; i++) {
    try {
        // request code
        break;
    } catch (Exception e) {
        // handle exception
        Thread.sleep(1000);
    }
}

2. Recursive Retry

Define a method that calls itself until the maximum retry count is reached.

public void requestWithRetry(int retryTimes) {
    if (retryTimes <= 0) return;
    try {
        // request code
    } catch (Exception e) {
        Thread.sleep(1000);
        requestWithRetry(retryTimes - 1);
    }
}

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.

CloseableHttpClient httpClient = HttpClients.custom()
    .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true))
    .build();
CloseableHttpClient httpClient = HttpClients.custom()
    .setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, NEG_ONE_SECOND))
    .build();

4. Spring Retry Library

Spring Retry provides annotations and templates for declarative or programmatic retries.

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.3.1</version>
</dependency>

Using RetryTemplate:

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;
});

Using annotations:

@Retryable(value = Exception.class, maxAttempts = 3)
public void request() {
    // request code
}

5. Resilience4j

Resilience4j offers a lightweight retry module that can be configured programmatically or via annotations.

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.7.0</version>
</dependency>

Programmatic example:

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");

Annotation example:

@Service
public class MyService {
    @Retryable(value = MyException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void doSomething() {
        // request 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.

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;
    }
}

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

Submit the request as a Callable to a thread pool and retry the task when a failure occurs.

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);
    }
}

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.

@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();
            }
        }
    }
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.