Create an Asynchronous Spring REST API with CompletableFutures and DeferredResult

Spring MVC is a powerful framework for building RESTful APIs. In accordance with the shift to asynchronous programming, Spring introduced the DeferredResult which an endpoint method can return so that its contents are propagated to the client when the asynchronous results of the request are eventually completed. Here’s a simple example:

 DeferredResult

@RestController
public class DeferredController {

  @RequestMapping
  public DeferredResult<String> get(@RequestParam String input) {
    DeferredResult<String> defResult = new DeferredResult<>();

    new Thread(() -> { 
      String apiResponse = callApi(input)
      deferredResult.setResult(apiResponse);
    }).start();

    return defResult;
  }

  String callApi(String str) {
    // restTemplate.invoke(...)
    sleep(1000);
    return str.toUpperCase();
  }
}

Using DeferredResult frees up the request thread to process other requests while the request waits for the asynchronous tasks to complete.

But what if we need to set the result of the DeferredResult based on an arbitrary number of API calls? Java 8’s CompletableFuture to the rescue. CompletableFuture was built to make handling complex asynchronous programming in Java easier. It lets the programmer combine and cascade asynchronous calls.

 CompletableFuture to set the DeferredResult asynchronously

Setting a DeferedResult using a CompletableFuture can be accomplished using the static methods CompletableFuture#runAsync or CompletableFuture#supplyAsync which will dispatch the async task to Java’s common thread pool by default.

DeferredResult<String> defResult = new DeferredResult<>();

runAsync(() -> {
  String apiResponse = callApi(inputStr);
  defResult.setResult(apiResponse);
});

return defResult;

 Two CompletableFutures to set the DeferredResult

But lets say one needs to make two API calls and concatenate the results. CompletableFuture#thenCombine makes short work of that.

DeferredResult<String> defResult = new DeferredResult<>();

CompletableFuture<String> f1 = supplyAsync(() -> 
  callApi("hello"));
CompletableFuture<String> f2 = supplyAsync(() -> 
  callApi("world"));

BiFunction<String, String, Void> bf = (s1, s2) -> {
  defResult.setResult(s1 + s2);
  return null;
};

f1.thenCombineAsync(f2, bf);

return defResult;

 Many CompletableFutures to set the DeferredResult

It’s been smooth sailing so far. Here’s where things get a bit dicey. Let’s combine N number of CompletableFuture.

Source code on GitHub

List<Input> userInput = asList("random", "user", "input")

// Create the collection of futures.
List<CompletableFuture<String>> futures =
  userInput.stream()
  .map(str -> supplyAsync(() -> callApi(str)))
  .collect(toList());

// Restructure as arr because CompletableFuture.allOf requires it.
CompletableFuture<?>[] futuresAsVarArgs = 
  futures.toArray(new CompletableFuture[futures.size()]);

// Create a new future to gather results once all of the previous futures complete.
CompletableFuture<Void> jobsDone = 
  CompletableFuture.allOf(futuresAsVarArgs);

// Once all of the futures have completed, build out the result string from the return values of the API calls.
DeferredResult<String> defResult = new DeferredResult<>();

jobsDone.thenAccept(i -> {
  StringBuilder stringBuilder = new StringBuilder();
  futures.forEach(f -> {
  try {
    stringBuilder.append(f.get());
    } catch (Exception e) {}
  });

  defResult.setResult(stringBuilder.toString());
});

return defResult;

Running the code and testing it with

curl http://localhost:8080?input=darth,plaguies,the,wise

now asynchronously returns DARTHPLAGUIESTHEWISE about a second later. Neat.

 Conclusion

CompletableFuture and DeferredResult are an excellent combination for creating a fully asynchronous REST controller. After more than two or three CompletableFuture tasks need to be combined, the code becomes a lot more difficult to work with. If that becomes the case, it may be worth considering a different concurrency model such as an actor system (Akka) or an event bus (RxJava).

 
55
Kudos
 
55
Kudos

Now read this

Step-by-Step Guide to Deploying a Full-Stack Spring Boot Application in AWS

Building and deploying a full-stack web application into the cloud can be a challenge even for experienced software engineers. Often times we become so specialized in front-end, back-end, or dev-ops that releasing even a trivial... Continue →

Subscribe to Carl Martensen

Don’t worry; we hate spam with a passion.
You can unsubscribe with one click.

F62tWw9EZzv86AwAMvUg