How to make asynchronous API requests in Java using CompletableFutures

November 12, 2020
Written by
Reviewed by

Java 8 was released in 2014, and introduced a raft of new language features such as Lambdas and the Streams API. A lot has happened since 2014 - Java is now at version 15, but industry surveys consistently report 8 as the most widely-used version, with very few developers using 7 or lower.

In October this year, the Twilio Java Helper Library was updated to use Java 8 features in release 8.0.0. This new major version reflects the fact that the library no longer supports Java 7.

One Java 8 API which sometimes gets overlooked is the CompletionStage API, usually accessed through the CompletableFuture class. The CompletionStage API lets programmers define pipelines of asynchronous operations for data, and handles the asynchronous behaviour for you. You define what you want to happen, and Java takes care of when it can happen.

In this post I'll show how you can use CompletableFutures with the new Twilio Helper Library; in fact, the same principles can be applied to deal with any asynchronous code. Java 11's HttpClient has async methods that return CompletableFuture instances, for example.

Synchronous and Asynchronous code

If you're calling the Twilio API to send an SMS, your code might look like this:

Message msg = Message.creator(
        MY_CELLPHONE_NUMBER,
        MY_TWILIO_NUMBER,
        "Hoot Hoot 🦉")
    .create();

[full code on GitHub]

This code will call the Twilio API, enqueue the SMS, and return a Message object which encapsulates details of the response, including the Message's SID which can be used later to look up what happened to the message. There's a lot going on behind the scenes there, including a request and response whizzing over the Internet - on my computer this call takes about a second to complete, and this code will wait until the Message is available before continuing. This is a synchronous (or "blocking") call.

There might be other things that your code could be doing while the API request is in progress, so how about if we made the call asynchronous? That would mean our code can continue to do other things and we can get hold of the Message later on when we need it. Replacing .create() with .createAsync() will do just that.

Futures

The .createAsync() method returns a Future. Similar to promises in other languages, Futures are objects that will contain a result when it's ready. The work is being done in a background thread, and when you need the result you can call a method on the Furture to get the result. When you call that method you might still have to wait, but your code has had a chance to do other things in the meantime.

From version 8.0.0 of the Twilio Helper Library, the type of future returned is now a CompletableFuture which has a .join() method for getting its result. So your code might look like this:

CompletableFuture<Message> msgFuture = Message.creator(
        MY_CELLPHONE_NUMBER,
        MY_TWILIO_NUMBER,
        "Hoot Hoot 🦉")
    .createAsync();

System.out.println("This is printed while the request is taking place");

Message msg = msgFuture.join(); // you might have to wait here
System.out.println("Message SID is " + msg.getSid());
Output of the code above: "This is printed..." above "Message SID is..."

So far, so good - but what makes the CompletionStage API special is that you can build up pipelines of code where each stage will be executed when it is ready, without you having to code the nuts and bolts of the asynchronous behaviour yourself. This is similar to how you can use callbacks in other languages, but more flexible as we will see.

Examples

OK, that description might have seemed a little complex. Here are a few examples which should help clarify:

Chaining computation sequentially

If you want to run some code after the API call has completed, use .thenApply(). The .thenApply() method takes a lambda or a method reference which transforms the value and returns another CompletionStage so you can chain more things if you need. When you want the final result, again you can call .join():

CompletableFuture<String> resultFuture =
    Message.creator(MY_CELLPHONE_NUMBER, MY_TWILIO_NUMBER, "Hoot Hoot 🦉")
        .createAsync()
        .thenApply(msg ->    // .thenApply can take a lambda
            writeToDatabase(msg.getTo(), msg.getSid(), msg.getDateCreated())
        ).thenApply(         // .thenApply can also take a method reference
            DatabaseResult::getValue);

System.out.println("Making the API call then writing the result to the DB happen in the background");

System.out.println("The final result was " + resultFuture.join());

[full code on GitHub]

Parallel execution

Extending the previous example, imagine you need to make several API requests - they don't depend on each other so it doesn't matter what order the requests happen in, but you do need to know when they are all complete for some final bookkeeping (writing what has happened into a database, for example).

You can schedule code to run after multiple CompletionStages have finished using CompletableFuture.allOf(). The lambda you pass to .allOf() takes no arguments; to get the results of each stage, use .join() in the lambda's body:

// makeApiRequestThenStoreResult contains the same code as the previous example
CompletableFuture<String> result1 = makeApiRequestThenStoreResult(CUSTOMER_1);
CompletableFuture<String> result2 = makeApiRequestThenStoreResult(CUSTOMER_2);

// those calls are all happening in the background

CompletableFuture.allOf(result1, result2)
        .thenRun(() ->
            System.out.printf("The final results were: %s and %s",
                              result1.join(), result2.join()));

[full code on GitHub]

Handling Errors in CompletionStages

If any exceptions are thrown in your asynchronous code, the CompletionStage API will catch them and let you handle them in a few different ways. If you do not handle them at all, then your call to .join() could throw a CompletionException which has the original exception as its cause.

A better way to recover might be to use the .handle() method - you provide a lambda which takes two arguments, a result and an exception. If the exception is non-null you can handle it here. .handle() returns a CompletableFuture so you can continue chaining or .join() to get the result:

CompletableFuture<String> msgFuture = makeApiRequestThenStoreResult(MY_CELLPHONE_NUMBER);

msgFuture.handle((s, ex) ->{
    if (ex != null){
        return "Failed: " + ex.getMessage();
    } else {
        return s;
    }
});

// all of the above happens in the background

System.out.println("The final result is " + msgFuture.join());

[full code on GitHub]

The full CompletionStage API

These small examples just scratch the surface of the CompletionStage API

scratching the surface (of a dog)

There are dozens of methods for chaining and combining asynchronous actions in different ways.

For more examples of what you can do with CompletableFutures, I recommend you check out the official documentation or this handy list of 20 examples.

Summary

Java 8's CompletionStage API gives us Java developers powerful tools for defining complex asynchronous processes, and is just one of the many additions to the newest Twilio Java Helper Library.

If you're using Twilio and Java I'd encourage you to update to the latest Helper library. If you're building with Twilio and Java let me know about it. I can't wait to see what you build.

🐦 @MaximumGilliard

📧 mgilliard@twilio.com