5 Essential CompletableFuture Patterns for Java Developers: Boost Asynchronous Programming

Aarav Joshi - Jan 24 - - Dev Community

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

As a Java developer, I've found CompletableFuture to be an invaluable tool for managing asynchronous operations. It's a game-changer in the world of concurrent programming, offering a more intuitive and flexible approach compared to traditional threading mechanisms.

CompletableFuture, introduced in Java 8, represents a significant leap forward in Java's concurrency model. It allows us to write asynchronous code that's both readable and maintainable, a combination that was often challenging to achieve with earlier concurrency tools.

Let's dive into the five essential patterns for leveraging CompletableFuture in Java applications, complete with code examples and practical insights.

Chaining Asynchronous Operations

One of the most powerful features of CompletableFuture is its ability to chain operations. This pattern allows us to create a pipeline of asynchronous tasks, each dependent on the result of the previous one. The methods thenApply, thenAccept, and thenRun are the workhorses of this pattern.

Here's a practical example:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Simulate fetching data from a remote server
    return "Raw data";
})
.thenApply(data -> {
    // Process the fetched data
    return data.toUpperCase();
})
.thenApply(processedData -> {
    // Further processing
    return "Processed: " + processedData;
});

// Block and get the result
String result = future.get();
System.out.println(result); // Outputs: Processed: RAW DATA
Enter fullscreen mode Exit fullscreen mode

In this example, we start with an asynchronous operation to fetch data, then process it in two stages. Each stage is executed asynchronously, but the code reads as if it were synchronous, making it much easier to understand and maintain.

The thenApply method is used when we want to transform the result and pass it to the next stage. If we don't need to return a value, we can use thenAccept, which consumes the result, or thenRun, which simply runs a Runnable regardless of the result.

Combining Multiple Futures

Often, we need to work with multiple asynchronous operations concurrently. CompletableFuture provides the allOf and anyOf methods for this purpose.

allOf is used when we need to wait for all the specified futures to complete:

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> "!");

CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2, future3);

combinedFuture.get(); // Wait for all futures to complete

// Now we can safely get the results
String result = future1.get() + " " + future2.get() + future3.get();
System.out.println(result); // Outputs: Hello World!
Enter fullscreen mode Exit fullscreen mode

anyOf, on the other hand, completes as soon as any of the given futures completes:

CompletableFuture<Object> future = CompletableFuture.anyOf(
    CompletableFuture.supplyAsync(() -> {
        TimeUnit.SECONDS.sleep(2);
        return "Result from long operation";
    }),
    CompletableFuture.supplyAsync(() -> "Quick result")
);

System.out.println(future.get()); // Likely outputs: Quick result
Enter fullscreen mode Exit fullscreen mode

These methods are particularly useful when dealing with multiple independent operations that can be executed concurrently.

Handling Exceptions Asynchronously

Exception handling in asynchronous code can be tricky, but CompletableFuture provides elegant solutions. The exceptionally and handle methods allow us to manage errors without blocking.

Here's an example using exceptionally:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() < 0.5) throw new RuntimeException("Oops!");
    return "Success";
})
.exceptionally(ex -> "Error: " + ex.getMessage());

System.out.println(future.get()); // Outputs either "Success" or "Error: Oops!"
Enter fullscreen mode Exit fullscreen mode

The handle method is even more flexible, allowing us to deal with both the result and the exception:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (Math.random() < 0.5) throw new RuntimeException("Oops!");
    return "Success";
})
.handle((result, ex) -> {
    if (ex != null) return "Error: " + ex.getMessage();
    return "Result: " + result;
});

System.out.println(future.get()); // Outputs either "Result: Success" or "Error: Oops!"
Enter fullscreen mode Exit fullscreen mode

These patterns ensure that our asynchronous code remains robust and error-resistant.

Timeouts and Cancellation

In real-world applications, we often need to deal with timeouts to prevent operations from running indefinitely. CompletableFuture provides the orTimeout and completeOnTimeout methods for this purpose.

Here's an example using orTimeout:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    return "Result";
})
.orTimeout(1, TimeUnit.SECONDS);

try {
    System.out.println(future.get());
} catch (ExecutionException e) {
    System.out.println("Operation timed out");
}
Enter fullscreen mode Exit fullscreen mode

In this case, the operation will timeout after 1 second, throwing a TimeoutException.

The completeOnTimeout method allows us to provide a default value in case of a timeout:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    return "Result";
})
.completeOnTimeout("Timeout occurred", 1, TimeUnit.SECONDS);

System.out.println(future.get()); // Outputs: "Timeout occurred"
Enter fullscreen mode Exit fullscreen mode

These methods are crucial for maintaining the responsiveness of our applications and managing resources effectively.

Asynchronous Completion Stages

Sometimes, we need more control over when and how a CompletableFuture completes. The complete, completeExceptionally, and obtrudeValue methods give us this control.

Here's an example using complete:

CompletableFuture<String> future = new CompletableFuture<>();

// Simulate an asynchronous operation
new Thread(() -> {
    try {
        TimeUnit.SECONDS.sleep(1);
        future.complete("Async operation completed");
    } catch (InterruptedException e) {
        future.completeExceptionally(e);
    }
}).start();

// Do some other work
System.out.println("Doing other work...");

// Wait for the result
System.out.println(future.get());
Enter fullscreen mode Exit fullscreen mode

This pattern is particularly useful when dealing with external events or complex asynchronous logic that doesn't fit neatly into the standard CompletableFuture methods.

The obtrudeValue method allows us to forcibly set the value of a CompletableFuture, even if it has already completed:

CompletableFuture<String> future = CompletableFuture.completedFuture("Initial value");
System.out.println(future.get()); // Outputs: Initial value

future.obtrudeValue("New value");
System.out.println(future.get()); // Outputs: New value
Enter fullscreen mode Exit fullscreen mode

This can be useful in testing scenarios or when we need to override the result of a CompletableFuture for some reason.

In my experience, these five patterns cover a wide range of use cases for asynchronous programming with CompletableFuture. They've helped me write cleaner, more efficient code that's easier to reason about and maintain.

One of the key benefits I've found is the ability to express complex asynchronous workflows in a way that's almost as straightforward as synchronous code. This has been particularly valuable when working on applications that interact with multiple external services or perform CPU-intensive operations.

However, it's worth noting that while CompletableFuture is a powerful tool, it's not always the best solution for every concurrency problem. For simple use cases, traditional threading mechanisms might be more appropriate. And for very complex scenarios involving many interdependent asynchronous operations, you might want to consider using a dedicated reactive programming library like RxJava or Project Reactor.

That said, for a wide range of common concurrency scenarios in Java applications, CompletableFuture strikes an excellent balance between power and simplicity. By mastering these patterns, you'll be well-equipped to handle most asynchronous programming challenges you're likely to encounter in your Java development work.

Remember, the key to effective use of CompletableFuture is to think in terms of asynchronous workflows rather than individual threads. This mindset shift can lead to more efficient, scalable, and maintainable code.

As with any powerful tool, it's important to use CompletableFuture judiciously. Overuse can lead to code that's hard to follow, so always consider the complexity of your use case and whether a simpler approach might suffice.

In conclusion, CompletableFuture represents a significant advancement in Java's concurrency toolkit. By leveraging these patterns, you can create robust, efficient, and readable asynchronous code, elevating the quality and performance of your Java applications.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .