Skip to content

Java Asynchronous Programming with Play

Nick Burgan edited this page Jun 13, 2025 · 5 revisions

In CiviForm, controllers and functions handling requests from applicants or trusted intermediaries are implemented asynchronously. Because we build CiviForm to scale to high QPS on the applicant side, we need to run this code asynchronously to handle that scale, or else the main application threads would get blocked waiting for slow functions like database queries to return and result in poor performance.

Writing Java asynchronous code is rather complex, and understanding how it works within the Play Framework adds a level of complexity on top of that. This aims to be a reference guide on how we use Java's CompletionStage/CompletableFuture mechanism with the Play Framework, but is not exhaustive of everything you can do with this and all of the functions available.

Setting the Stage

The Play Framework utilizes Pekko (formerly called Akka) to use an "Actor Model" for doing things asynchronously. If you read the docs, you might be scratching your head thinking "what does that even mean?". Think about it like a more traditional thread pool. You want to run a function asynchronously, you spawn a new thread to go run that function and then optionally process the result when it's finished. Pekko adds a layer of abstraction to this so you can say "Hey Pekko, here's my function I want you to go run, go do it and get back to me when you're finished." You don't have to set up the thread yourself, manage the thread's lifecycle, worry about using a volatile variable or mutex to write results back safely to the main thread, or any of that dirty business. Pekko just does it all for you.

Play has a mechanism called an Execution Context. Essentially what this boils down to is that each execution context has its own thread pool that Pekko manages. When you use Java's asynchronous functions to run in a particular execution context, Pekko handles getting a thread from that thread pool, running the code on a thread in that pool, and fetching and returning the result.

Play has a ClassLoaderExecutionContext by default. But you can also create your own custom execution contexts, which we do in CiviForm.

  • ClassLoaderExecutionContext - What we use to run most things asynchronously
  • DatabaseExecutionContext - Used by repository functions for making database queries
  • DurableJobExecutionContext - Used by Durable Jobs for executing in the background
  • ApiBridgeExecutionContext - For executing API Bridge functions (coming soon)
  • If you do not supply an execution context to these functions, they will use ForkJoinPool.commonPool() and not Play's dispatcher. While we do this in CiviForm, we should try to avoid it. See the note here for details.

For the custom contexts that we've created that extend CustomExecutionContext, which itself implements ExecutionContextExecutor, you can pass those contexts in directly to the async functions. For ClassLoaderExecutionContext, you must pass in the underlying Executor, which you can do via classLoaderExecutionContext.current().

Asynchronous stages

At a very base level, to run something asynchronously, you can pass a function (an existing one or a lambda) into runAsync or supplyAsync. Once that completes, you can then chain additional "stages" off of that original action by using a handful of different functions, depending on if you need to handle the output of the previous stage or not, or possibly the output of multiple stages.

CompletableFuture is an implementation of the CompletionStage interface. In fact, it is the only implementing class. CompletionStage could be used for implementing your own asynchronous library if you enjoy pain for some reason. Within CiviForm code, we have some functions that return a CompletionStage rather than a CompletableFuture (TODO: Why do we do this?). As such, you may need to use .toCompletableFuture() if you need to return or operate on a CompletableFuture.

runAsync

When you just need to run a chunk of code, and you don't need to get any results back, use runAsync. CiviForm Example

CompletableFuture<Void> future = CompletableFuture.runAsync(
  () -> {
    // Do some things here
  }, classLoaderExecutionContext.current());

supplyAsync

When you need to run a chunk of code and return its value, use supplyAsync. CiviForm Example

CompletableFuture<String> future = CompletableFuture.supplyAsync(
  () -> {
    return "hello!";
  }, classLoaderExecutionContext.current());

// This is equivalent
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello!", classLoaderExecutionContext.current());

Chaining functions

The below functions can be chained off of a CompletableFuture to do further actions. The ones listed here are all implementing a new asynchronous stage. There are also synchronous versions of these functions (e.g. thenRun), but there isn't usually a good reason to use them since we're attempting to do things asynchronously in the first place.

thenRunAsync

When you need to run a chunk of code after a previous stage completes, you don't need the use return values from the previous stage, and you don't want to return anything from this one, use thenRunAsync. CiviForm Example (Note, no longer in the codebase)

CompletableFuture<Void> future1 = CompletableFuture.supplyAsync(
  () -> {
    // Do some stuff
  }, classLoaderExecutionContext.current());

CompletableFuture<Void> future2 = future1.thenRunAsync(
  () -> {
    // Do some more stuff
  }, classLoaderExecutionContext.current());

thenAcceptAsync

When you need to run a chunk of code after the previous stage completes, you want to use the output of the previous stage, and not return anything from this stage, use thenAcceptAsync. CiviForm Example

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "hi!", classLoaderExecutionContext.current());

CompletableFuture<Void> future2 = future1.thenAcceptAsync(
  (s) -> {
    if(s.equals("hi!")) {
      // Yay!
    } else {
      // Boo!
    }
  }, classLoaderExecutionContext.current());

thenApplyAsync

When you need to run a chunk of code after the previous stage completes, you want to use the output of the previous stage, and you want to return a value from this stage, use thenApplyAsync. CiviForm Example

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "hi", classLoaderExecutionContext.current());

CompletableFuture<String> future2 = future1.thenApplyAsync(
  (s) -> {
    return s + "there!";
  }, classLoaderExecutionContext.current());

// Another example
CompletableFuture<String> future2 = future1.thenApplyAsync(String::toLowerCase, classLoaderExecutionContext.current()).toCompletableFuture();

thenComposeAsync

Sometimes, the code you want to run in a particular stage returns a CompletableFuture (or CompletionStage) itself. If you were to use a thenApplyAsync that returns a CompletableFuture, you'd then end up returning CompletableFuture<CompletableFuture<T>>. The thenComposeAsync function flattens this so it returns a simple CompletableFuture<T>. This function requires passing in the result of the previous stage, so if your previous stage doesn't output anything, you need to pass a bogus v variable. CiviForm Example

private CompletableFuture<String> getHiAsync(){
  return CompletableFuture.supplyAsync(() -> "hi!", classLoaderExecutionContext.current());
}

CompletableFuture<String> future = CompletableFuture.runAsync(
  () -> {
    // Do some stuff
  }, classLoaderExecutionContext.current())
  .thenComposeAsync(v -> getHiAsync(), classLoaderExecutionContext.current());

thenCombineAsync

Sometimes, you want to do two asynchronous functions that don't rely on each other, then use the output of both. For this case, you can use thenCombineAsync. CiviForm Example

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "foo", classLoaderExecutionContext.current());
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "bar", classLoaderExecutionContext.current());

CompletableFuture<String> future3 = future1
  .thenCombineAsync(
    future2,
      (f, b) -> {
        return f + b;
      }, classLoaderExecutionContext.current());

Additional Functions

completedFuture

In the branching logic of your function that returns a CompletableFuture, if you want to return a value early without having to do asynchronous work, you can return completedFuture(value). CiviForm Example

private CompletableFuture<Optional<String>> getFirstName(Optional<String> lastName){
  if(lastName.isEmpty()){
    return CompletableFuture.completedFuture(Optional.empty());
  }
  return lookupFirstName(lastName.get()); // A long running function that returns CompletableFuture
}

allOf

If you have multiple CompletableFutures that do not depend on each other, and then you want to do something after all of them complete, use allOf. Because allOf does not supply any of the return values of the futures into later stages, in order to get the values from those stages, you can do .join() from a previous stage inside of a staged chained off of allOf. You almost never should do .join() normally, but this is a case where it is safe to do so, as by the time this stage executes, you know the previous stages have completed and .join() will not block. CiviForm Example

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "foo", classLoaderExecutionContext.current());
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "bar", classLoaderExecutionContext.current());
CompletableFuture<String> future3 = CompletableFuture.allOf(future1, future2).thenApplyAsync(
  v -> future1.join() + future2.join(), classLoaderExecutionContext.current());

Error Handling

If an exception is thrown inside an asynchronous stage, a CompletionException will be thrown. This exception is unchecked, meaning you can compile without handling this potential exception. While our best practice is to always handle exceptions that can happen in async code, in practice, we have not been consistent with this.

To catch the exception and handle it gracefully, use CompletableFuture.exceptionally(ex -> { ... }). CiviForm Example

Additional Resources

Clone this wiki locally