Using CompletableFuture with RestTemplates in Spring-Boot Part 1

Share on:

Use case

RestTemplate is a synchronous web-client to perform http/s requests from spring applications.

Configuration

We first define the bean to be injected in our services using following config:

1@Configuration
2public class Config {
3
4    @Bean
5    public RestTemplate restTemplate() {
6        return new RestTemplate();
7    }
8
9}

Example

Suppose we have a couple of external apis returing some data given below:

 1@RestController
 2@RequestMapping(value = "/users")
 3public class UsersApis {
 4
 5    @GetMapping(value = "/profile")
 6    public Map<String, Object> profile() {
 7        return Map.of("profile",
 8                Map.of("name", "someone",
 9                        "active", true,
10                        "email", "user@gmail.com")
11        );
12    }
13
14    @GetMapping(value = "/address")
15    public Map<String, Object> address() {
16        return Map.of("address",
17                Map.of("state", "KA",
18                        "country", "India",
19                        "zipcode", "000 000")
20        );
21    }
22}

We now want our spring-app to call these api compose a single result pass it to the caller. The simple way to call these api one after the other and compose the response of each of this write to the caller. Here is how we can do it:

 1@RestController
 2@RequestMapping(value = "/compose")
 3@AllArgsConstructor
 4public class ApiCompositionController {
 5    private final RestTemplate restTemplate;
 6
 7    @GetMapping(value = "/simple")
 8    public Map<String, Object> compose() {
 9        return Map.of("status", "simple",
10                "profile", new UserProfileSupplier(restTemplate).get().map(UserProfileResponse::getProfile).orElse(new UserProfile()),
11                "address", new UserAddressSupplier(restTemplate).get().map(UserAddressResponse::getAddress).orElse(new UserAddress())
12        );
13    }
14}

Here UserProfileSupplier & UserAddressSupplier are given below which are nothing but classes implementing java.util.function.Supplier<T> interface for returning response from external apis:

 1@Slf4j
 2@AllArgsConstructor
 3public static class UserProfileSupplier implements Supplier<Optional<UserProfileResponse>> {
 4        private final RestTemplate restTemplate;
 5
 6
 7    @SneakyThrows
 8    @Override
 9    public Optional<UserProfileResponse> get() {
10        log.debug("Executing UserProfile in: " + Thread.currentThread().getName());
11        Thread.sleep(1000L);
12        final ResponseEntity<UserProfileResponse> response = restTemplate
13                    .getForEntity("http://localhost:8080/users/profile", UserProfileResponse.class);
14        if (response.hasBody()) {
15            return Optional.ofNullable(response.getBody());
16        }
17        return Optional.empty();
18    }
19}

And

 1@Slf4j
 2@AllArgsConstructor
 3public static class UserAddressSupplier implements Supplier<Optional<UserAddressResponse>> {
 4    private final RestTemplate restTemplate;
 5
 6    @SneakyThrows
 7    @Override
 8    public Optional<UserAddressResponse> get() {
 9       log.debug("Executing UserAddress in: " + Thread.currentThread().getName());
10        Thread.sleep(1000L);
11        final ResponseEntity<UserAddressResponse> response = restTemplate
12                .getForEntity("http://localhost:8080/users/address", UserAddressResponse.class);
13        if (response.hasBody()) {
14            return Optional.ofNullable(response.getBody());
15        }
16        return Optional.empty();
17    }
18}

For testing purpose we have added 1s delay to the caller of these apis. So when these 2 apis are called sequentially we cannot get composite result less than 2 seconds as these calls are happening sequentially.

Lets execute ab tool to benchmark these apis:

 1ab -n 1000 -c 100  http://localhost:8080/compose/simple
 2
 3Server Software:        
 4Server Hostname:        localhost
 5Server Port:            8080
 6
 7Document Path:          /compose/simple
 8Document Length:        158 bytes
 9
10Concurrency Level:      100
11Time taken for tests:   22.102 seconds
12Complete requests:      1000
13Failed requests:        0
14Total transferred:      263000 bytes
15HTML transferred:       158000 bytes
16Requests per second:    45.24 [#/sec] (mean)
17Time per request:       2210.237 [ms] (mean)
18Time per request:       22.102 [ms] (mean, across all concurrent requests)
19Transfer rate:          11.62 [Kbytes/sec] received
20
21Connection Times (ms)
22              min  mean[+/-sd] median   max
23Connect:        0    0   1.0      0       5
24Processing:  2001 2007   9.3   2003    2045
25Waiting:     2001 2006   9.1   2003    2045
26Total:       2002 2007  10.0   2003    2049
27
28Percentage of the requests served within a certain time (ms)
29  50%   2003
30  66%   2005
31  75%   2006
32  80%   2007
33  90%   2013
34  95%   2038
35  98%   2046
36  99%   2047
37 100%   2049 (longest request)

We are testing total 1000 requests and 100 concurrent request on server. As from the output we can see that the server is serving ~45 r/s. Lets not challenge this number as we a re calling an api which is internally other 2 apis whose response time is minimum is 1s. As the get() is a blocking call on suppliers. In the logs you can see that these api calls are being exeuted on thread http-nio-8080-exec-number.

In the next post we will compose these to api using completable future and see if we can do something better.

comments powered by Disqus