Techniques to Implement Retry Mechanisms for Failing API Calls in Microservices

6 min read

1. What Is a Retry Mechanism?
A retry mechanism allows a service to attempt a failed API call multiple times, with the hope that transient issues will resolve on subsequent tries. The system temporarily pauses the request, retrying after a specified interval. When carefully configured, retry mechanisms enhance service resilience, as temporary failures don't immediately disrupt the user experience.

Retry mechanisms help tackle common challenges in microservices, such as network outages, overloaded systems, and external dependency failures. This strategy is useful when failures are likely temporary, like network jitter or a momentarily busy server.
2. Types of Retry Strategies
Different retry strategies suit different microservices architectures. Let’s look at some core retry patterns and consider their applicability and potential issues.
2.1 Simple Retry
This is the most basic approach. A failed request is retried after a fixed time interval, which continues for a predefined number of attempts. Here’s a simple code example for implementing a retry mechanism with exponential backoff using Java’s CompletableFuture and a fixed interval between retries.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
public class SimpleRetry {
private static final int MAX_ATTEMPTS = 3;
private static final int DELAY = 2; // seconds
public static void main(String[] args) {
SimpleRetry simpleRetry = new SimpleRetry();
try {
simpleRetry.callApiWithRetry().get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public CompletableFuture<Void> callApiWithRetry() {
return attemptApiCall(1);
}
private CompletableFuture<Void> attemptApiCall(int attempt) {
return CompletableFuture.runAsync(() -> {
try {
if (attempt <= MAX_ATTEMPTS) {
System.out.println("Attempt " + attempt);
simulateApiCall();
}
} catch (Exception e) {
if (attempt < MAX_ATTEMPTS) {
try {
System.out.println("Retrying after " + DELAY + " seconds...");
TimeUnit.SECONDS.sleep(DELAY);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
attemptApiCall(attempt + 1).join();
} else {
System.err.println("API call failed after " + attempt + " attempts.");
}
}
});
}
private void simulateApiCall() throws Exception {
// Simulate a failure
throw new Exception("Simulated API failure");
}
}
In this example, the attemptApiCall method retries the API call up to three times with a 2-second delay between attempts. While simple, this approach can cause traffic spikes during heavy failures, potentially worsening an already busy system.
2.2 Exponential Backoff
Exponential backoff is a commonly recommended retry strategy that gradually increases the delay between attempts. This helps reduce network congestion by slowing down retry requests when the system faces issues. Exponential backoff is especially useful in cases of server throttling and network congestion.
In Java, we can implement an exponential backoff strategy as follows:
import java.util.concurrent.TimeUnit;
public class ExponentialBackoffRetry {
private static final int MAX_ATTEMPTS = 3;
private static final int INITIAL_DELAY = 1; // seconds
public static void main(String[] args) {
ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry();
exponentialBackoffRetry.callApiWithExponentialBackoff();
}
public void callApiWithExponentialBackoff() {
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
System.out.println("Attempt " + attempt);
simulateApiCall();
return;
} catch (Exception e) {
int delay = INITIAL_DELAY * (int) Math.pow(2, attempt - 1);
System.out.println("Retrying in " + delay + " seconds...");
try {
TimeUnit.SECONDS.sleep(delay);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
}
}
System.err.println("API call failed after all attempts.");
}
private void simulateApiCall() throws Exception {
throw new Exception("Simulated API failure");
}
}
This implementation calculates the delay based on an exponential formula, gradually increasing with each retry. This method efficiently minimizes impact on the server and network.
3. Considerations for Retry Mechanisms in Microservices
Implementing retry mechanisms effectively involves careful configuration and forethought. Let’s explore some essential considerations.
Idempotency
Ensure that the API being retried is idempotent, meaning it produces the same result no matter how many times it’s called. Non-idempotent APIs may lead to unintended consequences if retried, like duplicate transactions.
Timeout Management
Retries should have a limit to prevent endless loops. Combining retries with circuit breaking can further ensure that downstream services are not overwhelmed by frequent retry requests.
Circuit Breaker Integration
Incorporating a circuit breaker with retries is highly recommended. A circuit breaker prevents repeated requests to a failing service by temporarily "breaking the circuit" until the service recovers. In Spring Boot, you can implement this easily using Resilience4j:
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.vavr.control.Try;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
public class CircuitBreakerWithRetry {
private final RestTemplate restTemplate = new RestTemplate();
private final CircuitBreaker circuitBreaker;
private final Retry retry;
public CircuitBreakerWithRetry() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.build();
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(500))
.build();
this.circuitBreaker = CircuitBreaker.of("backendService", circuitBreakerConfig);
this.retry = Retry.of("backendService", retryConfig);
}
public void callServiceWithRetryAndCircuitBreaker() {
Try.ofSupplier(Retry.decorateSupplier(retry,
CircuitBreaker.decorateSupplier(circuitBreaker, this::callBackendService)))
.onFailure(System.out::println)
.get();
}
private String callBackendService() {
return restTemplate.getForObject("https://example.com/api", String.class);
}
public static void main(String[] args) {
new CircuitBreakerWithRetry().callServiceWithRetryAndCircuitBreaker();
}
}
Monitoring and Logging
Monitoring retry attempts is essential to gain insights into system performance and determine if retries need tuning. This can be achieved through logging retry attempts, success rates, and failures. Using tools like Prometheus or Grafana to visualize retry metrics can provide valuable insights.
4. Best Practices for Retry Mechanisms
Retry mechanisms can enhance microservices reliability, but best practices must be followed to ensure they work optimally.
Use Exponential Backoff with Jitter
Adding jitter, or randomness, to exponential backoff intervals reduces traffic spikes by spreading retry requests over time. This approach minimizes chances of all services retrying simultaneously.
Limit the Number of Retries
Set a maximum retry limit based on your service’s tolerance for repeated requests. High retry counts can worsen failures, so it’s best to implement a balance between service resilience and avoiding overloading.
Keep Retries Configurable
Make retry configurations adjustable, allowing fine-tuning based on evolving requirements. Consider storing retry configurations in a central location or using a configuration management tool.
Handle Downstream Dependencies Carefully
Retries can amplify issues if downstream services are unprepared for repeated requests. Use circuit breakers to prevent compounding failures across services.
5. Conclusion
Implementing a retry mechanism for failed API calls in a microservices setup can improve resilience, but it requires thoughtful configuration to avoid inadvertently overwhelming your systems. We discussed several techniques and best practices, including exponential backoff, circuit breakers, and logging to achieve a balanced and effective retry strategy. With careful planning and monitoring, you can handle transient errors gracefully and maintain robust microservices.
If you have any questions or want to share your experience with retry mechanisms, please comment below.
Read more at : Techniques to Implement Retry Mechanisms for Failing API Calls in Microservices
0
Subscribe to my newsletter
Read articles from Tuanhdotnet directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tuanhdotnet
Tuanhdotnet
I am Tuanh.net. As of 2024, I have accumulated 8 years of experience in backend programming. I am delighted to connect and share my knowledge with everyone.