How to make asynchronous API requests in Java using CompletableFutures
Time to read: 4 minutes
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:
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:
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()
:
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:
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:
The full CompletionStage API
These small examples just scratch the surface of the CompletionStage API
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.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.