Ensuring API Resilience in Spring Microservices Using Retry and Fallback Mechanisms
Implement retry mechanisms and fallback methods in Spring microservices to handle API failures, ensuring enhanced reliability and improved user experience.
Join the DZone community and get the full member experience.
Join For FreeIn the digital landscape of today, applications heavily rely on external HTTP/REST APIs for a wide range of functionalities. These APIs often orchestrate a complex web of internal and external API calls. This creates a network of dependencies. Therefore, when a dependent API fails or undergoes downtime, the primary application-facing API needs adeptly handle these disruptions gracefully. In light of this, this article explores the implementation of retry mechanisms and fallback methods in Spring microservices. Specifically, it highlights how these strategies can significantly bolster API integration reliability and notably improve user experience.
Understanding Dependent API Failures
Mobile and web applications consuming APIs dependent on other services for successful execution face unique challenges. For instance, calls to dependent APIs can fail for a variety of reasons, including network issues, timeouts, internal server errors, or scheduled downtimes. As a result, such failures can compromise user experience, disrupt crucial functionalities, and lead to data inconsistencies. Thus, implementing strategies to gracefully handle these failures is vital for maintaining system integrity.
Retry Mechanisms
As a primary solution, retry mechanisms serve to handle transient errors and temporary issues. By automatically reattempting an API call, this mechanism can often resolve problems related to brief network glitches or temporary server unavailability. Importantly, it's crucial to differentiate between scenarios suitable for retries, such as network timeouts, and those where retries might be ineffective or even detrimental, like business logic exceptions or data validation errors.
Retry Strategies
Common approaches include:
- Fixed interval retries: Attempting retries at regular intervals
- Exponential backoff: This strategy involves increasing the interval between retries exponentially, thereby reducing the load on the server and network.
Moreover, both methods should be accompanied by a maximum retry limit to prevent infinite loops. Additionally, it’s essential to monitor and log each retry attempt for future analysis and system optimization.
Retry and Fallback in Spring Microservices
There are 2 ways in which we can implement the Retry and Fallback method.
1. resilience4j
@Retry
annotation is a declarative way designed to simplify the implementation of retry logic in applications. This annotation is available in the resilience4j package. By applying this annotation to service methods, Spring handles the retry process automatically when specified types of exceptions are encountered.
The following is a real implementation example. The method calls an API to pull the bureau report for underwriting a loan application. If this method fails, the entire loan application underwriting process fails, impacting the consuming application, such as a mobile application. So we have annotated this method with @Retry
:
@Override
@Retry(name = "SOFT_PULL", fallbackMethod = "performSoftPull_Fallback")
public CreditBureauReportResponse performSoftPull(SoftPullParams softPullParams, ErrorsI error) {
CreditBureauReportResponse result = null;
try {
Date dt = new Date();
logger.info("UnderwritingServiceImpl::performSoftPull method call at :" + dt.toString() + ", for loan acct id:" + softPullParams.getUserLoanAccountId());
CreditBureauReportRequest request = this.getCreditBureauReportRequest(softPullParams);
RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate("SOFT_PULL", error);
HttpHeaders headers = this.getHttpHeaders(softPullParams);
HttpEntity<CreditBureauReportRequest> entity = new HttpEntity<>(request, headers);
long startTime = System.currentTimeMillis();
String uwServiceEndPoint = "/transaction";
String callUrl = String.format("%s%s", appConfig.getUnderwritingTransactionApiPrefix(), uwServiceEndPoint);
ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(callUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
result = responseEntity.getBody();
long endTime = System.currentTimeMillis();
long timeDifference = endTime - startTime;
logger.info("Time taken for API call SOFT_PULL/performSoftPull call 1: " + timeDifference);
} catch (HttpClientErrorException exception) {
logger.error("HttpClientErrorException occurred while calling SOFT_PULL API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (HttpStatusCodeException exception) {
logger.error("HttpStatusCodeException occurred while calling SOFT_PULL API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (Exception ex) {
logger.error("Error occurred in performSoftPull. Detail error:", ex);
throw ex;
}
return result;
}
We can define the other attributes like the number of retries and delays between retries in the application.yml file:
resilience4j.retry:
configs:
default:
maxRetryAttempts: 3
waitDuration: 100
externalPartner:
maxRetryAttempts: 2
waitDuration: 1000
instances:
SOFT_PULL:
baseConfig: externalPartner
We specify the fallback method fallbackMethod = "performSoftPull_Fallback"
. This method is invoked if all the configured retry attempts fail; in this case, two.
public CreditBureauReportResponse performSoftPull_Fallback(SoftPullParams softPullParams, ErrorsI error, Exception extPartnerException) {
logger.info("UnderwritingServiceImpl::performSoftPull_Fallback - fallback , method called for soft pull api call");
CreditBureauReportResponse creditBureauReportResponse = null;
String loanAcctId = softPullParams.getUserLoanAccountId();
ApplicantCoBorrowerIdsMapping applicantCoBorrowerIdsMapping = this.uwCoBorrowerRepository.getApplicantCoBorrowerIdsMapping(loanAcctId);
try {
boolean result = this.partnerServiceExceptionRepository.savePartnerServiceException(applicantCoBorrowerIdsMapping.getApplicantUserId(),
applicantCoBorrowerIdsMapping.getLoanId(), PartnerService.SOFT_PULL.getValue(), "GDS", null);
if (!result) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - Unable to save entry in the partner service exception table.");
}
LoanSubStatus loanSubStatus = LoanSubStatus.PARTNER_API_ERROR;
result = this.loanUwRepository.saveLoanStatus(applicantCoBorrowerIdsMapping.getApplicantUserId(), applicantCoBorrowerIdsMapping.getLoanId(),
IcwLoanStatus.INITIATED.getValue(), loanSubStatus.getName(), "Partner Service Down", null);
if (!result) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - Unable to update loan status, sub status when partner service is down.");
}
} catch (Exception ex) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - An error occurred while calling softPullExtPartnerFallbackService, detail error:", ex);
}
creditBureauReportResponse = new CreditBureauReportResponse();
UnderwritingApiError underwritingApiError = new UnderwritingApiError();
underwritingApiError.setCode("IC-EXT-PARTNER-1001");
underwritingApiError.setDescription("Soft Pull API error");
List<UnderwritingApiError> underwritingApiErrors = new ArrayList<>();
underwritingApiErrors.add(underwritingApiError);
creditBureauReportResponse.setErrors(underwritingApiErrors);
return creditBureauReportResponse;
}
In this scenario, the fallback method returns the same response object as the original method. However, we also record in our data storage that the service is down and save state, and relay an indicator back to the consumer service method. This indicator is then passed on to the consuming mobile application, alerting the user about issues with our partner services. Once the issue is rectified, we utilize the persisted state to resume the workflow and send a notification to the mobile application, indicating that normal operations can continue.
2. spring-retry
In this case, we need to install the spring-retry and spring-aspects packages. For the same method as above, we will replace it with @Retry
annotation:
@Retryable(retryFor = {HttpClientErrorException.class, HttpStatusCodeException.class, Exception.class}, maxAttempts = 2, backoff = @Backoff(delay = 100))
public CreditBureauReportResponse performSoftPull(SoftPullParams softPullParams, ErrorsI error) {
The @Retryable
annotation in Spring allows us to specify multiple exception types that should trigger a retry. We can list these exception types in the value attribute of the annotation.
To write a fallback method for our @Retryable
annotated method performSoftPull
, we would use the @Recover
annotation. This method is invoked when the performSoftPull
method exhausts its retry attempts due to the specified exceptions (HttpClientErrorException, HttpStatusCodeException, Exception
). The @Recover
method should have a matching signature to the @Retryable
method, with the addition of the exception type as the first parameter.
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(HttpClientErrorException ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(HttpStatusCodeException ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(Exception ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
Conclusion
In summary, in Spring microservices, effectively handling API failures with retry mechanisms and fallback methods is essential for building robust, user-centric applications. These strategies ensure the application remains functional and provides a seamless user experience, even in the face of API failures. By implementing retries for transient issues and defining fallback methods for more persistent failures, Spring applications can offer reliability and resilience in today’s interconnected digital world.
Opinions expressed by DZone contributors are their own.
Comments