eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

1. Overview

In this article, we’ll explore the details of the CompletableFuture.allOf() method and understand the differences between using it and calling join() on multiple separate CompletableFuture instances. We’ll discover that allOf() enables us to proceed with our flow in a non-blocking way while also ensuring atomicity.

2. join() vs. allOf()

CompletableFuture is a powerful feature introduced in Java 8, facilitating and promoting the creation of non-blocking code. In this article, we’ll focus on two methods that enable parallel code execution: join() and allOf().

Let’s start by analyzing the inner workings of these two methods. Following that, we’ll delve into their distinct approaches to accomplishing a common goal, executing code in parallel, and subsequently merging the results. For the code snippet of this article, we’ll use two helper functions that are blocking the thread for a period of time and then return some data or throw an exception:

private CompletableFuture waitAndReturn(long millis, String value) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(millis);
            return value;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
}

private CompletableFuture waitAndThrow(long millis) {
    return CompletableFuture.supplyAsync(() -> {
        try {
            Thread.sleep(millis);
        } finally {
            throw new RuntimeException();
        }
    });
}

2.1. join()

CompletableFuture API exposes the join() method as a way of retrieving the value of the Future object by blocking the thread until the execution is completed. We should notice that the caller thread will be blocked even if the execution of the CompletableFuture is happening on a different thread:

CompletableFuture<String> future = waitAndReturn(1_000, "Harry");
assertEquals("Harry", future.join());

Furthermore, if the CompletableFuture completes with an error,  join() will throw it as a RuntimeException: 

CompletableFuture<String> futureError = waitAndThrow(1_000);
assertThrows(RuntimeException.class, futureError::join);

2.2. allOf()

The static method allOff() allows us to combine multiple CompletableFuture instances and returns a CompletableFuture<Void>. The completion of the resulting objects is dependent on the completion of all subsequent Futures. Moreover, if any of the subsequent Futures completes exceptionally, the overall result will also be considered a failure. It’s important to understand that allOf() is not a blocking method, which means that it will be executed instantly:

CompletableFuture<String> f1 = waitAndReturn(1_000, "Harry");
CompletableFuture<String> f2 = waitAndReturn(2_000, "Ron");

CompletableFuture<Void> combinedFutures = CompletableFuture.allOf(f1, f2);

However, in order to extract the values, we’ll need to call additional methods of the API. For example, if we call join() on the resulting CompletableFuture<Void>, the thread will wait for the two composing CompletableFuture objects to complete – each in its own thread. In other words, the caller thread will be blocked the same amount of time as it takes for the longes Future to complete:

combinedFutures.join();

Since the main thread already waited the two seconds, f1 and f2 are now completed, and the subsequent calls like join() or get() will be executed immediately:

assertEquals("Harry", f1.join());
assertEquals("Ron", f2.join());

2.3. Executing Code in Parallel

As we can notice from the previous examples, we can execute CompletableFutures in parallel and combine the results simply by calling join() on each of them:

CompletableFuture<String> f1 = waitAndReturn(1_000, "Harry");
CompletableFuture<String> f2 = waitAndReturn(2_000, "Ron");

sayHello(f1.join());
sayHello(f2.join());

Or by iterating through a Collection or Stream of CompletableFututres, calling the join() on each of them, and using their results:

Stream.of(f1, f2).map(CompletableFuture::join).forEach(this::sayHello);

The question at hand is whether using the static allOf() method before iterating and joining all the CompletableFututres will have any impact on the final result:

CompletableFuture.allOf(f1, f2).join();
Stream.of(f1, f2).map(CompletableFututre::join).forEach(this::sayHello);

There are two significant distinctions between the two approaches: error handling and the capacity to proceed in a non-blocking manner. Let’s delve into each of them and understand their particularities.

3. Error Handling

One of the main differences between the two approaches is that if we omit to invoke allOf, we’ll process the results of the CompletableFutures sequentially. Consequently, we can end up with partial processing of the values.

In other words, if one of the CompletableFutures throws an exception, it will break the chain and stop processing. In some cases, this can cause errors because the prior elements were already processed:

CompletableFuture<String> f1 = waitAndReturn(1_000, "Harry");
CompletableFuture<String> f2 = waitAndThrow(1_000);
CompletableFuture<String> f3 = waitAndReturn(1_000, "Ron");

Stream.of(f1, f2, f3)
  .map(CompletableFuture::join)
  .forEach(this::sayHello);

On the other hand, we can combine the three instances using allOf() and then invoke the join() method to achieve some sort of atomicity. By doing so, we either process all elements at once or none of them:

CompletableFuture.allOf(f1, f2, f3).join();
Stream.of(f1, f2, f3)
  .map(CompletableFuture::join) 
  .forEach(this::sayHello);

4. Non-Blocking Code

One of the advantages of allOf() is that it allows us to continue our flow in a non-blocking way. Since the return type is a CompletableFuture<Void>, we can use thenAccept() to process the data when it arrives without blocking the thread:

CompletableFuture.allOf(f1, f2, f3)
  .thenAccept(__ -> sayHelloToAll(f1.join(), f2.join(), f3.join()));

Similarly, if we need to merge the data from the different Futures, we can use the thenApply() method instead. For instance, we can concatenate the values of the three futures and continue the non-blocking flow with the resulting String:

CompletableFuture<String> names = CompletableFuture.allOf(f1, f2, f3)
  .thenApply(__ -> f1.join() + "," + f2.join() + "," + f3.join());

Moreover, if we don’t leave the async world and continue the CompletableFuture chain, we’ll be able to leverage its own mechanism for error handling and recovery, the exceptionally() method. For example, if one of the CompletableFututres completes with an exception, we can simply log it and continue the flow with a default value:

CompletableFuture<String> names = CompletableFuture.allOf(f1, f2, f3)
  .thenApply(__ -> f1.join() + "," + f2.join() + "," + f3.join())
  .exceptionally(err -> {
      System.out.println("oops, there was a problem! " + err.getMessage());
      return "names not found!";
  });

5. Conclusions

In this article, we learned how to use CompletableFuture‘s join() for parallel code execution while seamlessly merging the results. We also unveiled the advantages of allOf().join(), which allows us to process the data atomically. In other words, the flow will only continue when all the constituent CompletableFuture objects have been completed successfully.

Lastly, we discovered that we could use allOf() and omit to invoke join(). This will allow us to use the results of multiple CompletableFutures while continuing the non-blocking flow. We achieved this through other useful methods of the API, such as thenApply(), theAccept(), and exceptionally().

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook Jackson – NPI EA – 3 (cat = Jackson)