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
WireMockServeris created on port8081, matching the port specified in theFeignClientconfigurations forUserFeignClientandProductFeignClient. - In
@BeforeEach, we start the server and stub the necessary endpoints to return mock responses.
- A
- Stubbing Endpoints
- Product Endpoint: We stub the
/products/1endpoint to return a JSON response with product details, simulating a successful call to retrieve product information. - User Endpoint: We stub the
/users/1endpoint to return a JSON response with user details.
- Product Endpoint: We stub the
- Test Method
- The
processOrder_returnsExpectedResult()test method callsorderService.processOrder(1L, 1L)and asserts that the response matches the expected output. - The assertions verify that the
OrderServicecorrectly processes the response from both Feign clients, confirming that asynchronous calls and data handling work as intended.
- The
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 theuserFuturechain. - If a
404 Not Foundoccurs, 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.
You can download the full source code of this example here: feign client completablefuture spring boot




