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

1. Overview

In this tutorial, we’ll explore the asynchronous execution support in Spring and the @Async annotation, utilizing modern Java and Spring 7 practices.

Simply put, annotating a method of a bean with @Async will execute it in a separate thread. In other words, the caller will not wait for the called method to complete, making the application more responsive and efficient.

One interesting aspect of Spring is that the event support in the framework also has support for async processing if necessary.

2. Enable Async Support

Let’s start by enabling asynchronous processing with Java configuration.

We’ll do this by adding the @EnableAsync to a configuration class:

@Configuration
@EnableAsync
public class SpringAsyncConfig { ... }

Although the @EnableAsync annotation is enough, there are also a few simple options for configuration as well:

  • annotationUsed to detect other, user-defined annotation types for async execution besides Spring’s @Async.
  • modeIndicates the type of advice to use (JDK proxy-based or AspectJ weaving).
  • proxyTargetClass Indicates the type of proxy to use (CGLIB or JDK). This is only effective if the mode is set to AdviceMode.PROXY.
  • order Sets the order in which AsyncAnnotationBeanPostProcessor should be applied. By default, it runs last so that it can take into account all existing proxies.

Note that XML configuration for enabling async support is generally avoided in modern Spring Boot 4 applications, favoring Java configuration.

3. The @Async Annotation

First, let’s go over the two main limitations of @Async:

  • It must be applied to public methods only.
  • Self-invocationcalling the async method from within the same class—won’t work because it bypasses the Spring proxy that intercepts the call to execute it asynchronously.

The reasons are simple. The method needs to be public so it can be proxied. Self-invocation doesn’t work because it bypasses the proxy and calls the underlying method directly.

3.1. Methods with void Return Type

This is the simple way to configure a method that doesn’t need to return a value to run asynchronously:

@Async
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. " 
      + Thread.currentThread().getName());
}

Since this is void, we typically assert that the calling thread continues immediately, but for a simple integration test, invoking it is sufficient:

@Autowired
private AsyncComponent asyncAnnotationExample; 

@Test
public void testAsyncAnnotationForMethodsWithVoidReturnType() {
    asyncAnnotationExample.asyncMethodWithVoidReturnType();
}

3.2. Methods With Return Type: Using CompletableFuture

For methods with a return type, Spring 7 and Spring Boot 4 strongly recommend using CompletableFuture. Further, the older AsyncResult is now deprecated.

By returning a CompletableFuture, we gain powerful composition and chaining capabilities, making asynchronous operations much easier to manage:

@Async
public CompletableFuture<String> asyncMethodWithReturnType() {
    System.out.println("Execute method asynchronously - " 
      + Thread.currentThread().getName());
    try {
        Thread.sleep(5000);
        return CompletableFuture.completedFuture("hello world !!!!");
    } catch (InterruptedException e) {
        return CompletableFuture.failedFuture(e);
    }
}

Now, let’s invoke the method and retrieve the result using the CompletableFuture object:

@Autowired 
private SimpleAsyncService simpleAsyncService;

@Test
public void testAsyncAnnotationForMethodsWithReturnType()
  throws InterruptedException, ExecutionException {
 
    CompletableFuture<String> future = simpleAsyncService.asyncMethodWithReturnType();
    System.out.println("Invoking an asynchronous method. " 
      + Thread.currentThread().getName());
    
    while (true) {
        if (future.isDone()) {
            System.out.println("Result from asynchronous process - " + future.get()); 
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }
}

3.3. Merging the Response of Two @Async Services

This example demonstrates how to use the CompletableFuture methods to combine the results from two separate asynchronous service calls. Let’s define two service classes, FirstAsyncService and SecondAsyncService, with an @Async annotated method:

@Async
public CompletableFuture<String> asyncGetData() throws InterruptedException {
    Thread.sleep(4000);
    return CompletableFuture.completedFuture(
        super.getClass().getSimpleName() + " response !!! "
    );
}

We’re now implementing the main service that we’ll use to merge the CompletableFuture responses of two @Async services:

@Service
public class AsyncService {

    @Autowired
    private FirstAsyncService firstService;
    @Autowired
    private SecondAsyncService secondService;

    public CompletableFuture<String> asyncMergeServicesResponse() throws InterruptedException {
        CompletableFuture<String> firstServiceResponse = firstService.asyncGetData();
        CompletableFuture<String> secondServiceResponse = secondService.asyncGetData();

        return firstServiceResponse.thenCompose(
          firstServiceValue -> secondServiceResponse.thenApply(
            secondServiceValue -> firstServiceValue + secondServiceValue));
    }
}

Let’s invoke the above service and retrieve the result of the asynchronous services using the CompletableFuture object:

@Autowired
private AsyncService asyncServiceExample;

@Test
public void testAsyncAnnotationForMergedServicesResponse()
  throws InterruptedException, ExecutionException {
    CompletableFuture<String> completableFuture = asyncServiceExample
        .asyncMergeServicesResponse();

    System.out.println("Invoking asynchronous methods. " + Thread.currentThread().getName());

    while (true) {
        if (completableFuture.isDone()) {
            System.out.println("Result from asynchronous process - " + completableFuture.get());
            break;
        }
        System.out.println("Continue doing something else. ");
        Thread.sleep(1000);
    }
}

Let’s check the output of the AsyncServiceUnitTest integration test class for merged services response:

Invoking asynchronous methods. main
Continue doing something else.
Continue doing something else.
Continue doing something else.
Continue doing something else.
Result from asynchronous process - FirstAsyncService response !!! SecondAsyncService response !!!

4. The Executor

By default, Spring uses a SimpleAsyncTaskExecutor to actually run these methods asynchronously. This is fine for development; however, for production, we should configure a proper thread pool, like ThreadPoolTaskExecutor, to manage resource consumption.

We can override the defaults at two levels: the application level or the individual method level.

4.1. Overriding the Executor at the Method Level

We need to declare the required executor as a Spring bean in a configuration class:

@Configuration
@EnableAsync
public class SpringAsyncConfig {
    
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("CustomPool-");
        executor.initialize();
        return executor;
    }
}

Then, we need to provide the executor name as an attribute of the @Async annotation:

@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - "
      + Thread.currentThread().getName());
}

4.2. Overriding the Executor at the Application Level

For this, the configuration class should implement the AsyncConfigurer interface. Accordingly, this forces it to implement the getAsyncExecutor() method, which will return the default executor for all methods annotated with @Async across the application (unless overridden at the method level):

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
   
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.initialize();
        return executor;
    }
    
    // ...
}

5. Exception Handling

When a method returns a CompletableFuture, exception handling is straightforward. An exception thrown inside the async method will cause the CompletableFuture to complete exceptionally, and any subsequent then… stages will handle it, or calling future.get() will throw the wrapped exception (ExecutionException).

However, if the method return type is void, exceptions will not be propagated back to the calling thread. For this scenario, we must register a custom handler.

Let’s create a custom async exception handler by implementing AsyncUncaughtExceptionHandler:

public class CustomAsyncExceptionHandler
  implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.err.println("Async Exception Detected!");
        System.err.println("Exception message - " + throwable.getMessage());
        System.err.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.err.println("Parameter value - " + param);
        }
    }
}

Finally, let’s register this handler by overriding the getAsyncUncaughtExceptionHandler() method in the AsyncConfigurer implementation:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
    // ... getAsyncExecutor() implementation ...

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

Let’s check the output of the integration test class AsyncAnnotationExampleIntegrationTest:

Invoking an asynchronous method. main
Continue doing something else.
Execute method asynchronously - DefaultAsync-1
Continue doing something else.
Continue doing something else.
Continue doing something else.
Continue doing something else.
Result from asynchronous process - hello world !!!!
Execute method with configured executor - CustomPool-1
Execute method asynchronously. DefaultAsync-2
Async Exception Detected!
Exception message - Throw message from asynchronous method.
Method name - asyncMethodWithExceptions

6. Conclusion

In this article, we looked at running asynchronous code with Spring 7 and Spring Boot 4.

We embraced the modern approach by using CompletableFuture for return types and looked at the core configuration using @EnableAsync and AsyncConfigurer, along with custom executor and exception handling strategies.

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.

Course – LS – NPI – (cat=Spring)
announcement - icon

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

>> CHECK OUT THE COURSE

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