Enterprise Java

Async Feign Client Calls in Spring Boot Using CompletableFuture

Using CompletableFuture with Feign Client in Spring Boot enables asynchronous HTTP calls, improving performance by allowing the application to process other tasks concurrently while waiting for a response. This approach can be valuable for microservices or applications that rely heavily on external HTTP services, where multiple calls may need to be processed simultaneously.

In this article, we will cover how to integrate CompletableFuture with Feign Client in a Spring Boot application.

1. Project Setup

To begin, a new Spring Boot project should be created, and the necessary dependencies for Feign Client should be added. For projects using Maven, the following dependencies can be included in the pom.xml.

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. Define Feign Client Interfaces

We will start by creating two Feign Client interfaces. These clients will interact with different endpoints on the same server (localhost:8081). Each client will be annotated with @FeignClient and will specify the same url attribute but target different endpoints.

The first client, UserFeignClient, will handle requests related to user data.

@FeignClient(name = "userClient", url = "http://localhost:8081")
public interface UserFeignClient {

    @GetMapping("/users/{id}")
    User getUserById(@PathVariable("id") Long id);
}

In this setup, UserFeignClient is used to fetch user information. Here, UserFeignClient defines a method getUserById that fetches user details from /users/{id}.

The second client, ProductFeignClient, will be used for requests related to product data. It is also pointed to localhost:8081 but targets a different endpoint.

@FeignClient(name = "productClient", url = "http://localhost:8081")
public interface ProductFeignClient {

    @GetMapping("/products/{id}")
    String getProductById(@PathVariable("id") Long id);
}

In this setup, ProductFeignClient is used to fetch product information. Here, ProductFeignClient defines a method getProductById that fetches product details from /products/{id}.

These Feign clients are going to be used by an OrderService class to fetch data asynchronously using CompletableFuture.

2.1 Define the Model Classes

Create the User and Product model classes to represent the data returned from the /users and /products endpoints.

public class User {
    private Long id;
    private String name;
    private String username;
    private String email;

    // Getters and Setters
}
public class Product {
    private Long id;
    private String name;
    private String category;
    private Double price;

    // Getters and Setters
}

3. Create a Service to Use Both Feign Clients

Now, we will create a service class, OrderService, which uses both UserFeignClient and ProductFeignClient asynchronously to fetch data.

@Service
public class OrderService {

    private final UserFeignClient userFeignClient;
    private final ProductFeignClient productFeignClient;

    public OrderService(UserFeignClient userFeignClient, ProductFeignClient productFeignClient) {
        this.userFeignClient = userFeignClient;
        this.productFeignClient = productFeignClient;
    }

    public String processOrder(Long userId, Long productId) throws ExecutionException, InterruptedException {
        // Fetch user asynchronously
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> 
            userFeignClient.getUserById(userId).getName()
        );

        // Fetch product asynchronously
        CompletableFuture<Void> productFuture = CompletableFuture.runAsync(() -> 
            System.out.println("Product details: " + productFeignClient.getProductById(productId))
        );

        // Wait for the userFuture to complete and retrieve the user's name
        return String.format("Order processed for user %s", userFuture.get());
    }
}

The OrderService class handles order processing by fetching user and product details asynchronously using CompletableFuture. It has two dependencies, UserFeignClient and ProductFeignClient, which are injected through the constructor. The processOrder() method accepts a userId and productId and fetches the user’s name and product details asynchronously. The method returns a string that includes the user’s name once the user data is fetched.

CompletableFuture.supplyAsync() is used for the user-fetching task. This method initiates an asynchronous task that returns a result, in this case, the user’s name from the userFeignClient.getUserById(). The result is obtained with userFuture.get(), which blocks the main thread until the task completes.

The main difference is that supplyAsync() returns a result, while runAsync() performs an action without returning a value. Both are non-blocking but serve different purposes. supplyAsync() for tasks that return results and runAsync() for tasks that don’t.

Let’s now write an integration test for OrderService using WireMock to mock the responses from the UserFeignClient and ProductFeignClient endpoints, and verify that the service processes the order correctly based on these responses.

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = CompleteablefuturefeignclientApplication.class)
public class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    private WireMockServer wireMockServer;

    @BeforeEach
    public void initWireMock() {
        wireMockServer = new WireMockServer(8081);
        configureFor("localhost", 8081);
        wireMockServer.start();

        // Stub the product endpoint
        stubFor(get(urlEqualTo("/products/1"))
                .willReturn(aResponse().withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", "application/json")
                        .withBody("{ \"id\": 1, \"name\": \"Laptop\", \"price\": 1500 }")));

       // Stub the user endpoint
        stubFor(get(urlEqualTo("/users/1"))
                .willReturn(aResponse().withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", "application/json")
                        .withBody("{ \"id\": 1, \"name\": \"Mr Fish\" }")));
    }

    @AfterEach
    public void stopWireMock() {
        wireMockServer.stop();
    }

    @Test
    void processOrder_returnsExpectedResult() throws ExecutionException, InterruptedException {
        // Stub the user endpoint
        stubFor(get(urlEqualTo("/users/1"))
                .willReturn(aResponse().withStatus(HttpStatus.OK.value())
                        .withHeader("Content-Type", "application/json")
                        .withBody("{ \"id\": 1, \"name\": \"Mr Fish\" }")));

        // Call the processOrder method
        String result = orderService.processOrder(1L, 1L);

        // Assert the result is as expected
        assertNotNull(result);
        assertEquals("Order processed for user Mr Fish", result);
    }
}

Explanation of the Test Code

  • Setting Up WireMock
    • A WireMockServer is created on port 8081, matching the port specified in the FeignClient configurations for UserFeignClient and ProductFeignClient.
    • In @BeforeEach, we start the server and stub the necessary endpoints to return mock responses.
  • Stubbing Endpoints
    • Product Endpoint: We stub the /products/1 endpoint to return a JSON response with product details, simulating a successful call to retrieve product information.
    • User Endpoint: We stub the /users/1 endpoint to return a JSON response with user details.
  • Test Method
    • The processOrder_returnsExpectedResult() test method calls orderService.processOrder(1L, 1L) and asserts that the response matches the expected output.
    • The assertions verify that the OrderService correctly processes the response from both Feign clients, confirming that asynchronous calls and data handling work as intended.

This integration test ensures that OrderService interacts correctly with both UserFeignClient and ProductFeignClient and validates the CompletableFuture setup by verifying the expected response.

4. Error Handling with CompletableFuture

Consider a scenario where the GET /users/${id} request fails with a 404 Not Found status, indicating that no user details are available for the given user ID. In such cases, it can be helpful to handle the error gracefully, perhaps by providing a default value or an alternative response.

To handle this error in CompletableFuture, we can use the .exceptionally() method to specify a fallback response when the request fails. Below is an example of how this can be done:

    public String processOrder(Long userId, Long productId) throws ExecutionException, InterruptedException {
        // Fetch user asynchronously
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(()
                -> userFeignClient.getUserById(userId).getName())
                .exceptionally(ex -> {
                    // Handle 404 error or other exceptions by providing a default user name
                    if (ex.getCause() instanceof FeignException.NotFound
                            && ((FeignException) ex.getCause()).status() == 404) {
                        return "Default User";
                    }
                    // Re-throw other exceptions
                    throw new RuntimeException("Critical error encountered!", ex);
                });

In this example:

  • The .exceptionally() block catches errors in the userFuture chain.
  • If a 404 Not Found occurs, it returns "Default User" as a fallback.
  • For any other exception, it re-throws the error, allowing further handling up the chain.

Using .exceptionally() provides a way to handle specific errors without disrupting the main application flow, ensuring that orders can still be processed with alternative values when specific data is unavailable.

5. Conclusion

In this article, we explored how to integrate CompletableFuture with Feign Clients in Spring Boot to handle asynchronous processing. We demonstrated how to set up multiple Feign Clients, use CompletableFuture methods like supplyAsync() and runAsync() for concurrent calls, and handle errors gracefully using the .exceptionally() method.

6. Download the Source Code

This article covered using Feign Client with CompletableFuture in Spring Boot.

Download
You can download the full source code of this example here: feign client completablefuture spring boot

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button