Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 70

Mastering @Async for Asynchronous Processing

Recommended resources

Hello! Welcome to another lesson on building robust backend systems.

In our last lesson, we tackled performance by implementing a distributed cache with Redis. This strategy helps by avoiding the re-computation of expensive operations. Today, we're going to explore a different but equally important technique for building responsive and efficient applications: asynchronous processing.

Some tasks, like sending an email, generating a report, or calling a slow third-party API, can take a long time. If you perform these tasks synchronously during a web request, the user is left waiting, and a server thread is tied up, unable to handle other requests. This lesson will teach you how to use Spring's @Async annotation to run these long-running tasks in the background, freeing up the main request thread and significantly improving your application's responsiveness.

Async Programming with @Async in Spring Boot
This diagram illustrates the core idea of asynchronous processing. The main server thread can offload time-consuming tasks like sending emails or processing files to run in the background, allowing it to remain responsive to new incoming requests.

1. The What and Why of Asynchronous Execution

By default, when you call a method in Java, the execution of your code pauses until that method completes. This is synchronous execution. In a web application, this means the HTTP request thread is blocked until all the work is done.

Asynchronous execution allows you to "fire and forget" a method call. Your main thread initiates the task and immediately continues with its own work, without waiting for the background task to finish. This is ideal for operations that are not critical to the immediate response you need to send back to the user.

To get a formal introduction to how Spring enables this, let's turn to a well-regarded resource.

How To Do @Async in Spring

The Baeldung blog provides a concise overview of what the @Async annotation does in Spring.

Please read the short 'Overview' section (Section 1). It clearly states the primary purpose of @Async: to execute a method in a separate thread, making the overall application more responsive.

Now that you understand the goal, let's see how to implement it.


2. Implementing Asynchronous Methods with @Async

Putting @Async to work involves three main steps:

  1. Enabling asynchronous support in your application.
  2. Annotating a Spring bean's method with @Async.
  3. Handling the return value, if any.

The official Spring guide on this topic provides an excellent, self-contained example that we will walk through.

Step 1: Enable Asynchronous Processing

First, you need to tell Spring to activate its asynchronous method execution capability. This is done by adding the @EnableAsync annotation to one of your configuration classes, most commonly your main application class.

Step 2: Create an Asynchronous Service

Next, you create a method in a Spring bean (like a @Service) and mark it with @Async. A crucial aspect to understand is what happens when an asynchronous method needs to return a value. Since the method returns immediately, the value isn't available yet. This is where Java's CompletableFuture comes in.

A CompletableFuture<T> is a placeholder—a promise—for a result of type T that will be available at some point in the future.

Let's study the guide's example, which simulates looking up GitHub user data.

Getting Started | Creating Asynchronous Methods - Spring

This guide demonstrates how to create a service with an asynchronous method that calls an external API.

Please review the code for the GitHubLookupService.java class. Note how the findUser method is annotated with @Async. Most importantly, observe its return type: CompletableFuture<User>. The method returns this future object immediately, while the actual API call and processing happen in a background thread.

Step 3: Calling the Asynchronous Method

Now, how does the calling code interact with this? The caller receives the CompletableFuture objects instantly and can choose when and how to retrieve the results. This allows you to kick off multiple independent, long-running tasks in parallel.

Getting Started | Creating Asynchronous Methods - Spring

Let's look at the part of the guide that calls our new async service. This will highlight the performance benefits.

Study the AppRunner.java code. Notice that it calls gitHubLookupService.findUser() three times in a row. These calls return instantly. The key line is CompletableFuture.allOf(page1,page2,page3).join();. This tells the main thread to wait at that point until all three background tasks have completed. The example cleverly measures the total time, demonstrating that the three 1-second tasks complete in just over 1 second total, not 3.

This parallel execution is the real power of @Async. You can fire off multiple independent operations and then aggregate their results once they are all ready, drastically reducing the total wait time.


3. Important Rules and Common Pitfalls

The @Async annotation seems like magic, but its mechanism is based on Spring AOP (Aspect-Oriented Programming), which uses proxies. This implementation detail leads to a few critical rules you must follow.

  1. Public Methods Only: @Async only works on public methods. Spring cannot proxy and intercept calls to private or protected methods.
  2. No Self-Invocation: An @Async method call must come from an outside object. If you call an @Async method from another method within the same class (this.myAsyncMethod()), the call will bypass the proxy and execute synchronously. This is a very common mistake for beginners.

How To Do @Async in Spring

The Baeldung article explains these limitations and the reasons behind them very clearly.

Please read the beginning of Section 3, 'The @Async Annotation'. It explicitly lists the two main limitations (public methods and no self-invocation) and explains that they are due to the proxy-based nature of the feature.

Test your understanding!

A developer is building an OrderService. It has a public method placeOrder(). Inside placeOrder(), after saving the order to the database, it makes a call to this.sendOrderConfirmationEmail() to notify the user. The developer annotates sendOrderConfirmationEmail() with @Async to avoid making the user wait for the email to be sent.

Will the sendOrderConfirmationEmail() method run asynchronously? Why or why not?

Show answer

No, it will not run asynchronously. It will run as a normal, synchronous method call.

The reason is self-invocation. The call this.sendOrderConfirmationEmail() is made from within the OrderService object itself. This call does not go through the Spring-generated proxy that contains the asynchronous logic. It's a direct method call on the plain Java object, so the @Async annotation has no effect.

To fix this, the developer would need to move the sendOrderConfirmationEmail() method to a separate bean (e.g., EmailService) and inject EmailService into OrderService.


Conclusion

You've now added another powerful tool to your backend development toolkit. By offloading long-running tasks to background threads, you can build applications that are more responsive and provide a better user experience.

Key Takeaways:

  • Asynchronous methods, enabled by @Async, prevent long-running tasks from blocking the main application thread.
  • You must enable this feature globally with the @EnableAsync annotation.
  • For async methods that need to return a value, use CompletableFuture<T> as the return type.
  • The caller can use CompletableFuture.allOf(...).join() to wait for multiple asynchronous tasks to complete in parallel.
  • @Async only works on public methods called from an external bean due to its proxy-based implementation. Self-invocation leads to synchronous execution.

What's Next?

In this lesson, we let Spring use its default settings for managing the background threads. While this is fine for simple cases, in a production environment, you need precise control over your thread pool—for instance, how many threads can run concurrently, how many tasks can be queued, and how threads are named for easier logging.

In our next lesson, we will dive into configuring a custom thread pool executor for your asynchronous operations, giving you the power to fine-tune your application's performance and resource management.