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. Introduction

In this tutorial, we’ll explore two important Java classes for handling tasks that need to run concurrently: ExecutorService and CompletableFuture. We’ll learn their functionalities and how to use them effectively, and we’ll understand the key differences between them.

2. Overview of ExecutorService

The ExecutorService is a powerful interface in Java’s java.util.concurrent package that simplifies managing tasks that need to run concurrently. It abstracts away the complexities of thread creation, management, and scheduling, allowing us to focus on the actual work that needs to be done.

ExecutorService provides methods like submit() and execute() to submit tasks we want to run concurrently. These tasks are then queued and assigned to available threads within the thread pool. If the task returns results, we can use Future objects to retrieve them. However, retrieving results using methods like get() on a Future can block the calling thread until the task is completed.

3. Overview of CompletableFuture

The CompletableFuture was introduced in Java 8. It focuses on composing asynchronous operations and handling their eventual results in a more declarative way.CompletableFuture acts as a container that holds the eventual result of an asynchronous operation. It might not have a result immediately, but it provides methods to define what to do when the result becomes available.

Unlike ExecutorService, where retrieving results can block the thread, CompletableFuture operates in a non-blocking manner.

4. Focus and Responsibilities

While both ExecutorService and CompletableFuture tackle asynchronous programming in Java, they serve distinct purposes. Let’s explore their respective focus and responsibilities.

4.1. ExecutorService

ExecutorService focuses on managing thread pools and executing tasks concurrently. It offers methods for creating thread pools with different configurations, such as fixed-size, cached, and scheduled.

Let’s see an example that creates and maintains exactly three threads using ExecutorService:

ExecutorService executor = Executors.newFixedThreadPool(3);
Future<Integer> future = executor.submit(() -> {
    // Task execution logic
    return 42;
});

The newFixedThreadPool(3) method call creates a thread pool with three threads, ensuring that no more than three tasks will be executed concurrently. The submit() method is then used to submit a task for execution in the thread pool, returning a Future object representing the result of the computation.

4.2. CompletableFuture

In contrast, CompletableFuture provides a higher-level abstraction for composing asynchronous operations. It focuses on defining the workflow and handling the eventual results of asynchronous tasks.

Here’s an example that uses supplyAsync() to initiate an asynchronous task that returns a string:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
    return 42;
});

In this example, supplyAsync() initiates an asynchronous task that returns a result of 42.

5. Chaining Asynchronous Tasks

Both ExecutorService and CompletableFuture offer mechanisms for chaining asynchronous tasks, but they take different approaches.

5.1. ExecutorService

In ExecutorService, we typically submit tasks for execution and then use the Future objects returned by these tasks to handle dependencies and chain subsequent tasks. However, this involves blocking and waiting for the completion of each task before proceeding to the next, which can lead to inefficiencies in handling asynchronous workflows.

Consider the case where we submit two tasks to an ExecutorService and then chain them together using Future objects:

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<Integer> firstTask = executor.submit(() -> {
    return 42;
});

Future<String> secondTask = executor.submit(() -> {
    try {
        Integer result = firstTask.get();
        return "Result based on Task 1: " + result;
    } catch (InterruptedException | ExecutionException e) {
        // Handle exception
    }
    return null;
});

executor.shutdown();

In this example, the second task depends on the result of the first task. However, ExecutorService doesn’t offer built-in chaining, so we need to explicitly manage the dependency by waiting for the first task to complete using get() — which blocks the thread — before submitting the second task.

5.2. CompletableFuture

On the other hand, CompletableFuture offers a more streamlined and expressive way to chain asynchronous tasks. It simplifies task chaining with built-in methods like thenApply(). These methods allow you to define a sequence of asynchronous tasks where the output of one task becomes the input for the next.

Here’s an equivalent example using CompletableFuture:

CompletableFuture<Integer> firstTask = CompletableFuture.supplyAsync(() -> {
    return 42;
});

CompletableFuture<String> secondTask = firstTask.thenApply(result -> {
    return "Result based on Task 1: " + result;
});

In this example, the thenApply() method is used to define the second task, which depends on the result of the first task. When we use thenApply() to chain a task to a CompletableFuture, the main thread doesn’t wait for the first task to complete before proceeding. It continues executing other parts of our code.

6. Error Handling

In this section, we’ll examine how both ExecutorService and CompletableFuture manage errors and exceptional scenarios.

6.1. ExecutorService

When using ExecutorService, errors can manifest in two ways:

  • Exceptions thrown within the submitted tasks: These exceptions propagate back to the main thread when we attempt to retrieve the result using methods like get() on the returned Future object. This can lead to unexpected behavior if not handled appropriately.
  • Unchecked exceptions during thread pool management: If an unchecked exception occurs during thread pool creation or shutdown, it’s typically thrown from the ExecutorService methods themselves. We need to catch and handle these exceptions in our code.

Let’s look at an example, highlighting the potential issues:

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<String> future = executor.submit(() -> {
    if (someCondition) {
        throw new RuntimeException("Something went wrong!");
    }
    return "Success";
});

try {
    String result = future.get();
    System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
    // Handle exception
} finally {
    executor.shutdown();
}

In this example, the submitted task throws an exception if a specific condition is met. However, we need to use a try-catch block around future.get() to catch exceptions thrown by the task or during retrieval using get(). This approach can become tedious for managing errors across multiple tasks.

6.2. CompletableFuture

In contrast, CompletableFuture offers a more robust approach to error handling with methods like exceptionally() and handling exceptions within the chaining methods themselves. These methods allow us to define how to handle errors at different stages of the asynchronous workflow, without the need for explicit try-catch blocks.

Here’s an equivalent example using CompletableFuture with error handling:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    if (someCondition) {
        throw new RuntimeException("Something went wrong!");
    }
    return "Success";
})
.exceptionally(ex -> {
    System.err.println("Error in task: " + ex.getMessage());
    return "Error occurred"; // Can optionally return a default value
});

future.thenAccept(result -> System.out.println("Result: " + result));

In this example, the asynchronous task throws an exception and the error is caught and handled within the exceptionally() callback. It provides a default value (“Error occurred”) in case of an exception.

7. Timeout Management

Timeout management is crucial in asynchronous programming to ensure that tasks are completed within a specified timeframe. Let’s explore how ExecutorService and CompletableFuture handle timeouts differently.

7.1. ExecutorService

ExecutorService doesn’t offer built-in timeout functionality. To implement timeouts, we need to work with Future objects and potentially interrupt tasks exceeding the deadline. This approach involves manual coordination:

ExecutorService executor = Executors.newFixedThreadPool(2);

Future<String> future = executor.submit(() -> {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        System.err.println("Error occured: " + e.getMessage());
    }
    return "Task completed";
});

try {
    String result = future.get(2, TimeUnit.SECONDS);
    System.out.println("Result: " + result);
} catch (TimeoutException e) {
    System.err.println("Task execution timed out!");
    future.cancel(true); // Manually interrupt the task.
} catch (Exception e) {
    // Handle exception
} finally {
    executor.shutdown();
}

In this example, we submit a task to the ExecutorService and specify a timeout of two seconds when retrieving the result using the get() method. If the task takes longer than the specified timeout to complete, a TimeoutException is thrown. This approach can be error-prone and requires careful handling.

It’s important to note that while the timeout mechanism interrupts the waiting for the task result, the task itself will continue running in the background until it either completes or is interrupted. For instance, to interrupt a task running within an ExecutorService, we need to use the Future.cancel(true) method.

7.2. CompletableFuture

In Java 9, CompletableFuture offers a more streamlined approach to timeouts with methods like completeOnTimeout(). The completeOnTimeout() method will complete the CompletableFuture with a specified value if the original task isn’t complete within the specified timeout duration.

Let’s look at an example that illustrates how completeOnTimeout() works:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        // Handle exception
    }
    return "Task completed";
});

CompletableFuture<String> timeoutFuture = future.completeOnTimeout("Timed out!", 2, TimeUnit.SECONDS);

String result = timeoutFuture.join();
System.out.println("Result: " + result);

In this example, the supplyAsync() method initiates an asynchronous task that simulates a long-running operation, taking five seconds to complete. However, we specify a timeout of two seconds using completeOnTimeout(). If the task isn’t completed within two seconds, the CompletableFuture will be automatically completed with the value “Timed out!”.

8. Summary

Here’s the comparison table summarizing the key differences between ExecutorService and CompletableFuture:

Feature ExecutorService CompletableFuture
Focus Thread pool management and task execution Composing asynchronous operations and handling eventual results
Chaining Manual coordination with Future objects Built-in methods like thenApply()
Error Handling Manual try-catch blocks around Future.get() exceptionally(), whenComplete(), handling within chaining methods
Timeout Management Manual coordination with Future.get(timeout) and potential interruption Built-in methods like completeOnTimeout()
Blocking vs. Non-Blocking Blocking (often waits for Future.get() to retrieve results) Non-blocking (chains tasks without blocking the main thread)

9. Conclusion

In this article, we’ve explored two essential classes for handling asynchronous tasks: ExecutorService and CompletableFuture. ExecutorService simplifies the management of thread pools and concurrent task execution, while CompletableFuture provides a higher-level abstraction for composing asynchronous operations and handling their results.

We’ve also examined their functionalities, differences, and how they handle error handling, timeout management, and chaining of asynchronous tasks.

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)