Create an Asynchronous REST API with Spring MVC and CompletableFutures

Spring MVC is a versatile framework for building RESTful APIs. Spring has long supported asynchronous endpoints through its DeferredResult, which frees up request processing threads to handle other requests while responses are built in the background. This can greatly increase throughput for IO intensive APIs. 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();
  }
}

 CompletableFuture

Java 8 introduced CompletableFuture which can be used with Spring to create async endpoints that similarly free up request threads to perform other tasks. DeferredResult#setResult may be simply replaced with CompletableFuture#complete.

@RequestMapping
public Future<String> get() {
  CompletableFuture<String> future = new CompletableFuture<>();
  future.complete("not actually in the background");
  return future;
}

CompletableFuture was built to make handling complex asynchronous programming easier. It lets the programmer combine and cascade async calls, and offers the static utility methods runAsync and supplyAsync to abstract away the manual creation of threads. These methods dispatch tasks to Java’s common thread pool by default or a custom thread pool if provided as an optional argument.

@RequestMapping
public Future<String> get(@RequestParam String input) {
  CompletableFuture<String> future = new CompletableFuture<>();
  return CompletableFuture.supplyAsync(() -> "in the background");
}

 Two CompletableFutures

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

public Future<Integer> get() {
  CompletableFuture<String> f1 = supplyAsync(() -> 
    callApi("1")); 
  CompletableFuture<String> f2 = supplyAsync(() -> 
    callApi("2"));

  BiFunction<String, String, Integer> bf = 
    (s1, s2) -> parseInt(s1) + parseInt(s2);

  return f1.thenCombineAsync(f2, bf);
}

 Many CompletableFutures

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

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.
CompletableFuture<String> output = new CompletableFuture<>();

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

  output.complete(stringBuilder.toString());
});

return output;

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.

 Be Careful To Complete Futures Upon Failure

If a CompletableFuture is returned by an endpoint method and #complete is never called, the request will hang until it times out. To prevent this, CompletableFuture#completeExceptionally should be used when error handling.

CompletableFuture<String> output = new CompletableFuture<>();
try {
  output.complete(parseInt("success"));
catch (Exception e) {
  output.completeExceptionally(e);
}
return output;

 What about @EnableAsync?

Spring also provides an @EnableAsync annotation to create async APIs.

This can be a great tool for simple async processing. The caveat is that only the first method called in an async class is async and it must be public. For example

@EnableAsync
public class AsyncService {
  @Async
  public String myEndpoint() {
    return "This is async";   
  }

  @Async
  String doSomething() {
    return "This is not async since its not public"
  }

  @Async
  public String doSomethingElse() {
    return "This is not async when called from myEndpoint()"
  }
}

As such it can be difficult to combine the results of multiple async tasks because classes have to be carefully structured to support that.

 Conclusion

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

 
723
Kudos
 
723
Kudos

Now read this

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

Feel free to email me if you have suggestions or questions about this tutorial or Spring Boot AWS setup in general. Introduction This guide starts with a simple Spring Boot hello world application, wires it into a database, and goes... Continue →