In this Spring boot tutorial, learn to use Spring retry module to invoke remote APIs and retry the request if it fails for some reason such as a network outage, server down, network glitch, deadlock etc. In such cases, we usually try to retry the operation a few times before sending an error to the client to make processing more robust and less prone to failure.
The spring-retry module provides a declarative way to configure the retries using annotations. We can also define the fallback method if all retries fail.
1. Maven
Import the latest version of spring-retry dependency from the maven repository. Spring retry is AOP based so include the latest version of spring-aspects as well.
<dependencies>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
</dependencies>
2. Enabling Spring Retry with @EnableRetry
The @EneableRetry annotation enables the spring retry feature in the application. We can apply it to any @Confguration class. The @EnableRetry scan for all @Retryable and @Recover annotated methods and proxies them using AOP. It also configures RetryListener interfaces used for intercepting methods used during retries.
@EnableRetry
@Configuration
public class RetryConfig { ... }
3. Spring Retry using Annotations
In spring-retry, we can retry the operations using the following annotations for the declarative approach.
3.3. @Retryable
It indicates a method to be a candidate for retry. We specify the exception type for which the retry should be done, the maximum number of retries and the delay between two retries using the backoff policy.
Please note that maxAttampts includes the first failure also. In other words, the first invocation of this method is always the first retry attempt. The default retry count is 3.
public interface BackendAdapter {
@Retryable(retryFor = {RemoteServiceNotAvailableException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public String getBackendResponse(String param1, String param2);
...
}
3.2. @Recover
It specifies the fallback method if all retries fail for a @Retryable method. The method accepts the exception that occurred in the Retryable method and its arguments in the same order.
public interface BackendAdapter {
...
@Recover
public String getBackendResponseFallback(RemoteServiceNotAvailableException e, String param1, String param2);
}
4. Using RetryTemplate for Pragammatic Approach
We can use the programmatic way for retry functionality creating RetryTemplate bean as follows:
We can create the template and use it as follows:
RetryTemplate template = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(RemoteServiceNotAvailableException.class)
.build();
template.execute(ctx -> {
return backendAdapter.getBackendResponse(...);
});
We can also configure the RetryTemplate bean with custom default values:
@EnableRetry
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
//Fixed delay of 1 second between retries
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
fixedBackOffPolicy.setBackOffPeriod(1000l);
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
//Retry only 3 times
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
return retryTemplate;
}
}
And now we use this bean for retry operations:
@Autowired
RetryTemplate retryTemplate;
template.execute(ctx -> {
return backendAdapter.getBackendResponse(...);
});
5. Spring Retry Example
In this example, we created a Spring boot project to expose one sample Rest API which will call one backend operation. The backed operation is prone to failure, and we will fail it randomly to simulate the service failures.
The BackendAdapterImpl class implements both @Retryable and @Recover methods as declared in BackendAdapter interface. We should always use the retry annotations on interfaces, so the implementations are automatically retry enabled.
public interface BackendAdapter {
@Retryable(retryFor = {RemoteServiceNotAvailableException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000))
public String getBackendResponse(String param1, String param2);
@Recover
public String getBackendResponseFallback(RemoteServiceNotAvailableException e, String param1, String param2);
}
@Slf4j
@Service
public class BackendAdapterImpl implements BackendAdapter {
@Override
public String getBackendResponse(String param1, String param2) {
int random = new Random().nextInt(4);
if (random % 2 == 0) {
throw new RemoteServiceNotAvailableException("Remote API failed");
}
return "Hello from remote backend!!!";
}
@Override
public String getBackendResponseFallback(RemoteServiceNotAvailableException e, String param1, String param2) {
return "Hello from fallback method!!!";
}
}
In the above code, the backend service will be retried 3 times. In those 3 attempts, if we get success response then that successful response will be returned else the fallback method will be called.
Create one simple RestController that will call the BackendAdapter.getBackendResponse() method where we have simulated the exception.
@RestController
public class ResourceController {
@Autowired
BackendAdapter backendAdapter;
@GetMapping("/retryable-operation")
public ResponseEntity<String> validateSpringRetryCapability(@RequestParam String param1, @RequestParam String param2) {
String apiResponse = backendAdapter.getBackendResponse(param1, param2);
return ResponseEntity.ok().body(apiResponse);
}
}
Invoke the REST API and watch the logs for retry attempts:
HTTP GET http://localhost:8080/retryable-operation?param1=param1¶m2=param2
The following were the logs when all 3 retries happened, and finally, the fallback method was invoked.
TRACE 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: RetryContext retrieved: [RetryContext: count=0, lastException=null, exhausted=false]
2022-12-09T13:24:04.348+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Retry: count=0
2022-12-09T13:24:05.363+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Checking for rethrow: count=1
2022-12-09T13:24:05.363+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Retry: count=1
2022-12-09T13:24:06.375+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Checking for rethrow: count=2
2022-12-09T13:24:06.375+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Retry: count=2
2022-12-09T13:24:06.375+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Checking for rethrow: count=3
2022-12-09T13:24:06.376+05:30 DEBUG 3824 --- [nio-8080-exec-1] o.s.retry.support.RetryTemplate: Retry failed last attempt: count=3
2022-12-09T13:24:06.392+05:30 TRACE 3824 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing ["Hello from fallback method!!!"]
The following are the logs when no failure occurred and API returned the response in the first attempt.
2022-12-09T13:24:31.411+05:30 TRACE 3824 --- [nio-8080-exec-2] o.s.retry.support.RetryTemplate : RetryContext retrieved: [RetryContext: count=0, lastException=null, exhausted=false]
2022-12-09T13:24:31.411+05:30 DEBUG 3824 --- [nio-8080-exec-2] o.s.retry.support.RetryTemplate : Retry: count=0
2022-12-09T13:24:31.413+05:30 TRACE 3824 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing ["Hello from remote backend!!!"]
7. RetryTemplate Use Cases in Spring
7.1. External REST API Calls
One of the most common use cases for RetryTemplate is making HTTP calls to external REST APIs. Network issues, temporary service unavailability, rate limiting, or timeout errors can cause API calls to fail intermittently. RetryTemplate automatically retries these failed requests with configurable delays and maximum attempts, significantly improving the reliability of your application’s integration with external services.
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.ResourceAccessException;
@Service
public class WeatherApiService {
private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate;
public WeatherApiService(RetryTemplate retryTemplate, RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
}
public String getWeatherData(String city) {
return retryTemplate.execute(context -> {
System.out.println("Attempt " + (context.getRetryCount() + 1) + " to fetch weather for " + city);
String url = "https://api.weather.com/v1/forecast?city=" + city;
return restTemplate.getForObject(url, String.class);
});
}
}
7.2. Database Operations with Transient Failures
Database operations can fail due to deadlocks, lock timeouts, connection pool exhaustion, or temporary network issues between your application and database server. RetryTemplate is extremely useful for handling these transient database errors that often resolve themselves after a brief wait.
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.dao.DeadlockLoserDataAccessException;
import org.springframework.transaction.annotation.Transactional;
@Repository
public class AccountRepository {
private final RetryTemplate retryTemplate;
private final JdbcTemplate jdbcTemplate;
public AccountRepository(RetryTemplate retryTemplate, JdbcTemplate jdbcTemplate) {
this.retryTemplate = retryTemplate;
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void transferFunds(Long fromAccount, Long toAccount, Double amount) {
retryTemplate.execute(context -> {
System.out.println("Attempting transfer - Attempt: " + (context.getRetryCount() + 1));
jdbcTemplate.update(
"UPDATE accounts SET balance = balance - ? WHERE account_id = ?",
amount, fromAccount
);
jdbcTemplate.update(
"UPDATE accounts SET balance = balance + ? WHERE account_id = ?",
amount, toAccount
);
jdbcTemplate.update(
"INSERT INTO transactions (from_account, to_account, amount, timestamp) VALUES (?, ?, ?, NOW())",
fromAccount, toAccount, amount
);
return null;
});
}
}
7.3. Message Queue Operations
When working with message brokers like RabbitMQ, Apache Kafka, or AWS SQS, publishing or consuming messages can fail due to network issues, broker unavailability, or connection timeouts. RetryTemplate ensures that critical messages aren’t lost due to these temporary failures by automatically retrying the send or receive operation.
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class OrderMessagingService {
private final RetryTemplate retryTemplate;
private final RabbitTemplate rabbitTemplate;
private final ObjectMapper objectMapper;
public OrderMessagingService(RetryTemplate retryTemplate,
RabbitTemplate rabbitTemplate,
ObjectMapper objectMapper) {
this.retryTemplate = retryTemplate;
this.rabbitTemplate = rabbitTemplate;
this.objectMapper = objectMapper;
}
public void publishOrderEvent(Order order) {
retryTemplate.execute(context -> {
System.out.println("Publishing order event - Attempt: " + (context.getRetryCount() + 1));
String orderJson = objectMapper.writeValueAsString(order);
rabbitTemplate.convertAndSend(
"orders.exchange",
"order.created",
orderJson
);
System.out.println("Order event published successfully for order: " + order.getId());
return null;
});
}
// Simple Order class for demonstration
static class Order {
private Long id;
private String customerId;
private Double amount;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
}
}
7.4. File System Operations
File system operations, especially when dealing with network file systems (NFS), cloud storage services (S3, Azure Blob), or distributed file systems, can fail due to temporary network issues, permission problems that resolve themselves, or resource contention. RetryTemplate helps handle these scenarios by retrying file read, write, or delete operations that fail transiently.
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
@Service
public class FileStorageService {
private final RetryTemplate retryTemplate;
public FileStorageService(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
}
public void writeToFile(String filename, String content) {
retryTemplate.execute(context -> {
System.out.println("Writing to file - Attempt: " + (context.getRetryCount() + 1));
Path filePath = Paths.get("/shared/storage/" + filename);
Files.createDirectories(filePath.getParent());
Files.writeString(filePath, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println("File written successfully: " + filename);
return null;
});
}
public String readFromFile(String filename) throws IOException {
return retryTemplate.execute(context -> {
System.out.println("Reading from file - Attempt: " + (context.getRetryCount() + 1));
Path filePath = Paths.get("/shared/storage/" + filename);
String content = Files.readString(filePath);
System.out.println("File read successfully: " + filename);
return content;
});
}
}
7.5. Cloud Service Integration (AWS, Azure, GCP)
Cloud service APIs often implement rate limiting, throttling, and can experience temporary service degradations or regional outages. RetryTemplate is essential when integrating with cloud services because it handles these scenarios gracefully using exponential backoff strategies.
For instance, AWS services return specific error codes when rate limits are exceeded, and the recommended practice is to retry with exponentially increasing delays. RetryTemplate can be configured to recognize these specific errors and apply appropriate retry strategies.
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.InputStream;
@Service
public class S3StorageService {
private final RetryTemplate retryTemplate;
private final AmazonS3 s3Client;
private final String bucketName = "my-application-bucket";
public S3StorageService(RetryTemplate retryTemplate, AmazonS3 s3Client) {
this.retryTemplate = retryTemplate;
this.s3Client = s3Client;
}
public void uploadFile(String key, File file) {
retryTemplate.execute(context -> {
System.out.println("Uploading to S3 - Attempt: " + (context.getRetryCount() + 1));
PutObjectRequest request = new PutObjectRequest(bucketName, key, file);
s3Client.putObject(request);
System.out.println("File uploaded successfully to S3: " + key);
return null;
});
}
public InputStream downloadFile(String key) {
return retryTemplate.execute(context -> {
System.out.println("Downloading from S3 - Attempt: " + (context.getRetryCount() + 1));
S3Object s3Object = s3Client.getObject(bucketName, key);
System.out.println("File downloaded successfully from S3: " + key);
return s3Object.getObjectContent();
});
}
}
7.6. Microservices Communication
In microservices architectures, services communicate with each other over the network, introducing multiple failure points including network partitions, service instance failures, or temporary overload conditions. RetryTemplate is crucial for making inter-service communication resilient by automatically retrying failed service calls.
When Service A calls Service B, a transient network issue or a momentary spike in Service B’s load might cause the call to fail. Instead of immediately propagating this failure to the end user, RetryTemplate retries the call after a brief delay, during which Service B might recover or the network issue might resolve.
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.HttpServerErrorException;
@Service
public class OrderService {
private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate;
private final String inventoryServiceUrl = "http://inventory-service:8080";
private final String paymentServiceUrl = "http://payment-service:8080";
public OrderService(RetryTemplate retryTemplate, RestTemplate restTemplate) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
}
public boolean checkInventory(String productId, int quantity) {
return retryTemplate.execute(context -> {
System.out.println("Checking inventory - Attempt: " + (context.getRetryCount() + 1));
String url = inventoryServiceUrl + "/api/inventory/check?productId=" + productId + "&quantity=" + quantity;
Boolean available = restTemplate.getForObject(url, Boolean.class);
System.out.println("Inventory check completed: " + available);
return available != null && available;
});
}
public String processPayment(PaymentRequest paymentRequest) {
return retryTemplate.execute(context -> {
System.out.println("Processing payment - Attempt: " + (context.getRetryCount() + 1));
String url = paymentServiceUrl + "/api/payments/process";
PaymentResponse response = restTemplate.postForObject(url, paymentRequest, PaymentResponse.class);
System.out.println("Payment processed: " + response.getTransactionId());
return response.getTransactionId();
});
}
// Simple DTO classes
static class PaymentRequest {
private String orderId;
private Double amount;
private String customerId;
public String getOrderId() { return orderId; }
public void setOrderId(String orderId) { this.orderId = orderId; }
public Double getAmount() { return amount; }
public void setAmount(Double amount) { this.amount = amount; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
}
static class PaymentResponse {
private String transactionId;
private String status;
public String getTransactionId() { return transactionId; }
public void setTransactionId(String transactionId) { this.transactionId = transactionId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}
}
7.7. Scheduled Jobs and Batch Processing
Scheduled jobs and batch processing applications often perform operations that can fail due to external dependencies being temporarily unavailable. RetryTemplate is invaluable in these scenarios because batch jobs typically run during off-peak hours, and any failure could mean waiting another 24 hours for the next execution.
import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
@Service
public class DataSyncScheduler {
private final RetryTemplate retryTemplate;
private final RestTemplate restTemplate;
private final JdbcTemplate jdbcTemplate;
public DataSyncScheduler(RetryTemplate retryTemplate,
RestTemplate restTemplate,
JdbcTemplate jdbcTemplate) {
this.retryTemplate = retryTemplate;
this.restTemplate = restTemplate;
this.jdbcTemplate = jdbcTemplate;
}
@Scheduled(cron = "0 0 2 * * *") // Run every day at 2 AM
public void syncCustomerData() {
System.out.println("Starting customer data synchronization job");
retryTemplate.execute(context -> {
System.out.println("Fetching customer data from external API - Attempt: " + (context.getRetryCount() + 1));
// Fetch data from external API
String apiUrl = "https://external-api.com/customers";
CustomerData[] customers = restTemplate.getForObject(apiUrl, CustomerData[].class);
System.out.println("Fetched " + customers.length + " customers, updating database...");
// Update database with retry
for (CustomerData customer : customers) {
jdbcTemplate.update(
"INSERT INTO customers (customer_id, name, email, last_updated) " +
"VALUES (?, ?, ?, NOW()) " +
"ON DUPLICATE KEY UPDATE name = ?, email = ?, last_updated = NOW()",
customer.getId(), customer.getName(), customer.getEmail(),
customer.getName(), customer.getEmail()
);
}
System.out.println("Customer data synchronization completed successfully");
return null;
});
}
@Scheduled(fixedDelay = 300000) // Run every 5 minutes
public void processFailedOrders() {
retryTemplate.execute(context -> {
System.out.println("Processing failed orders - Attempt: " + (context.getRetryCount() + 1));
List<Map<String, Object>> failedOrders = jdbcTemplate.queryForList(
"SELECT * FROM orders WHERE status = 'FAILED' AND retry_count < 3"
);
for (Map<String, Object> order : failedOrders) {
Long orderId = (Long) order.get("order_id");
// Retry processing the order
processOrder(orderId);
}
System.out.println("Processed " + failedOrders.size() + " failed orders");
return null;
});
}
private void processOrder(Long orderId) {
// Order processing logic
System.out.println("Reprocessing order: " + orderId);
}
// Simple DTO class
static class CustomerData {
private String id;
private String name;
private String email;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
}
8. Conclusion
This tutorial taught us to use the spring retry module for implementing retry-based remote API invocations that can handle infrequent runtime exceptions or network failures.
Happy Learning !!
Comments