Spring Boot 3 Resilience4j Retry Tutorial

Resilience4j is a lightweight fault tolerance library inspired by Netflix Hystrix but designed for Java 8 and functional programming. It provides several resilience patterns like Circuit Breaker, Rate Limiter, and Retry, which are particularly useful for building resilient microservices. The integration with Spring Boot allows developers to use these patterns with minimal configuration, leveraging Spring’s auto-configuration capabilities.

Setting Up Your Project

To get started, you’ll need to set up a Spring Boot 3 project:

1. Create a New Project

Use Spring Initializr to generate a new Spring Boot project. Select dependencies like Spring Web, Spring Boot Actuator, and Resilience4j.

2. Add Dependencies

Ensure your pom.xml includes the necessary Resilience4j dependencies. Here’s how it might look in Maven:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot3</artifactId>
    </dependency>
</dependencies>

Configuring Resilience4j Retry

Once your project is set up, you’ll configure the retry mechanism:

1. Application Configuration

Open your application.yml or application.properties file to add retry configurations. Here’s an example: yaml

resilience4j:
  retry:
    instances:
      myService:
        maxAttempts: 3
        waitDuration: 1s
        retryExceptions:
          - org.springframework.web.client.HttpServerErrorException
        ignoreExceptions:
          - org.springframework.web.client.HttpClientErrorException

This configuration sets up a retry instance named myService that will retry operations up to three times, waiting one second between attempts for server errors but ignoring client errors.

2. Implementing Retry in the Service Layer

In your service class, use the @Retry annotation:

@Service
@Slf4j
public class Resilience4jService {

    private final Random random = new Random();

    @Retry(name = "myService")
    public String callExternalService(String param) {
        log.info("Attempt #{}", LocalDateTime.now());

        // Simulate an external call that might fail
        if (random.nextBoolean()) {
            throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Service Unavailable");
        }
        return "Service response";
    }
}

This method will be retried according to the configuration if it throws an HttpServerErrorException.

Handling Fallbacks

Sometimes, you might want to provide a fallback method if all retries fail:

@Service
@Slf4j
public class Resilience4jService {

    private final Random random = new Random();

    @Retry(name = "myService", fallbackMethod = "fallback")
    public String callExternalService(String param) {
        log.info("Attempt #{}", LocalDateTime.now());

        // Simulate an external call that might fail
        if (random.nextBoolean()) {
            throw new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Service Unavailable");
        }
        return "Service response";
    }


    public String fallback(String param, Throwable throwable) {
        return "Fallback response due to: " + throwable.getMessage();
    }
}

Monitoring and Metrics

Resilience4j integrates with Spring Boot Actuator to provide metrics:

Enable Actuator: Ensure your application.yml exposes the relevant endpoints: yaml

    management:
      endpoints:
        web:
          exposure:
            include: "*"  # Exposes all actuator endpoints
      endpoint:
        health:
          show-details: always  # Show detailed health information

    Access Metrics: You can now access retry metrics via endpoints.

    http://localhost:8080/actuator/retryevents
    {
      "retryEvents": [
        {
          "retryName": "myService",
          "type": "RETRY",
          "creationTime": "2024-11-27T15:33:45.129577800+07:00[Asia/Bangkok]",
          "errorMessage": "org.springframework.web.client.HttpServerErrorException: 500 Service Unavailable",
          "numberOfAttempts": 2
        },
        {
          "retryName": "myService",
          "type": "ERROR",
          "creationTime": "2024-11-27T15:33:46.140762900+07:00[Asia/Bangkok]",
          "errorMessage": "org.springframework.web.client.HttpServerErrorException: 500 Service Unavailable",
          "numberOfAttempts": 3
        }
      ]
    }

    Practical Example

    Let’s consider a practical scenario where you’re making an API call to an external service that might be temporarily down or slow:

    @RestController
    @RequestMapping("/api/v1")
    @RequiredArgsConstructor
    public class MyController {
    
        private final Resilience4jService resilience4jService;
    
        @GetMapping("/resilience4j-retry")
        public ResponseEntity<ApiResponse> resilience4jRetry() {
            try {
                String result = resilience4jService.callExternalService("Hello world");
                return ResponseEntity.ok(new ApiResponse(true, result, null));
            } catch (Exception e) {
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .body(new ApiResponse(false, null, e.getMessage()));
            }
        }
    }

    Create a response bean.

    import lombok.AllArgsConstructor;
    import lombok.Data;
    
    @Data
    @AllArgsConstructor
    public class ApiResponse {
        private boolean success;
        private String data;
        private String error;
    }

    Test Resilience4j retry by Postman.

    Spring Boot application using Postman involves setting up a retry mechanism in your service and invoking it through a REST API endpoint.

    Test Resilience4j Retry

    1. The client sends a request to the REST API endpoint.

    http://localhost:8080/api/v1/resilience4j-retry
    request

    In the console log. Retry attempt 3 times.

    Attempt #2024-11-27T15:55:21.906816100
    Attempt #2024-11-27T15:55:22.916523200
    Attempt #2024-11-27T15:55:23.927661800
    response

    Test result: The server response error with HTTP status 500. because Retry attempts more than 3 times, but gets an exception every time.

    2. The client sends a request to the REST API endpoint.

    http://localhost:8080/api/v1/resilience4j-retry
    request

    In console log. Retry attempt 2 times.

    Attempt #2024-11-27T16:00:30.840048
    Attempt #2024-11-27T16:00:31.849122300
    response

    Test result: The server response error with HTTP status 200. because Retry attempts less than 3 times.

    Test Resilience4j Retry and fallback

    1. The client sends a request to the REST API endpoint.

    http://localhost:8080/api/v1/resilience4j-retry
    request

    In the console log. Retry attempt 3 times.

    Attempt #2024-11-27T16:04:23.088632800
    Attempt #2024-11-27T16:04:24.099379300
    Attempt #2024-11-27T16:04:25.110237700
    response

    Test result: The server response error with HTTP status 200. because Retry attempts more than 3 times and gets an error message from a fallback method instead.

    Best Practices

    • Idempotency: Ensure that retry operations are idempotent to avoid unintended side effects.
    • Backoff Strategies: Use exponential backoff or randomized wait times to prevent overwhelming the downstream service (enableRandomizedWait or enableExponentialBackoff in configurations).
    • Conditional Retries: Use conditions to retry only on specific outcomes or exceptions for more precise control (retryOnResult or retryOnException in configurations).

    Conclusion

    Integrating Resilience4j’s retry mechanism into a Spring Boot 3 application significantly enhances the resilience of your services. It helps manage transient failures, thereby improving the reliability and availability of your application. Following this tutorial, you’ve learned how to set up, configure, and utilize retry patterns effectively, ensuring your application can withstand temporary disruptions without compromising user experience.

    Explore other Resilience4j patterns like Circuit Breaker or Time Limiter for further learning to build even more robust applications. Remember, resilience isn’t just about handling failures but also about making your system more predictable and manageable in the face of adversity.

    Leave a Comment

    Your email address will not be published. Required fields are marked *