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 port8081
, matching the port specified in theFeignClient
configurations forUserFeignClient
andProductFeignClient
. - In
@BeforeEach
, we start the server and stub the necessary endpoints to return mock responses.
- A
- 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.
- 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
OrderService
correctly 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 theuserFuture
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.
You can download the full source code of this example here: feign client completablefuture spring boot