Spring Boot @Async Annotation | Run Tasks Asynchronously

🚀 Introduction: What is @Async in Spring Boot?

The @Async annotation in Spring Boot allows asynchronous execution of methods in a separate thread, improving performance for long-running operations without blocking the main thread.

Key Features of @Async:
✔ Runs methods asynchronously without blocking execution.
✔ Uses Spring’s TaskExecutor to manage threads.
✔ Works with CompletableFuture<T>, Future<T>, and void return types.
✔ Can be used for background tasks, I/O operations, and API calls.

📌 In this guide, you’ll learn:
How to enable and use @Async in Spring Boot.
How to return values asynchronously.
How to handle exceptions in async methods.

1️⃣ Enabling Async Execution in Spring Boot

Before using @Async, enable asynchronous execution in your Spring Boot application.

📌 Enable @Async in @Configuration Class

@Configuration
@EnableAsync // Enables async execution
public class AsyncConfig {
}

@EnableAsync allows Spring to process @Async methods asynchronously.

2️⃣ Basic Example: Running a Method Asynchronously

📌 Example: Running an Async Method in a Service

1. Create an Async Service (NotificationService.java)

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

    @Async
    public void sendEmail(String email) {
        System.out.println("Sending email to: " + email);
        try {
            Thread.sleep(3000); // Simulating delay
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Email sent to: " + email);
    }
}

2. Call Async Method from Controller (UserController.java)

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final NotificationService notificationService;

    public UserController(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    @PostMapping("/register")
    public String registerUser(@RequestParam String email) {
        notificationService.sendEmail(email); // Async method call
        return "User registered successfully. Email will be sent asynchronously.";
    }
}

📌 POST Request (POST /api/users/register?email=test@example.com)
📌 Console Output:

User registered successfully. Email will be sent asynchronously.
Sending email to: test@example.com
(After 3 seconds delay)
Email sent to: test@example.com

The API responds immediately while email sending happens in the background.

3️⃣ Using CompletableFuture<T> to Return Async Results

To return asynchronous results, use CompletableFuture<T>.

📌 Example: Fetching Data Asynchronously

1. Async Service (DataService.java)

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Service
public class DataService {

    @Async
    public CompletableFuture<String> fetchData() {
        try {
            Thread.sleep(2000); // Simulate delay
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return CompletableFuture.completedFuture("Fetched Data Successfully!");
    }
}

2. Controller (DataController.java)

@RestController
@RequestMapping("/api/data")
public class DataController {

    private final DataService dataService;

    public DataController(DataService dataService) {
        this.dataService = dataService;
    }

    @GetMapping
    public CompletableFuture<String> getData() {
        return dataService.fetchData(); // Async execution
    }
}

📌 GET Request (GET /api/data)
📌 Response (after 2 seconds delay):

Fetched Data Successfully!

The response is wrapped inside CompletableFuture<T> and executed asynchronously.

4️⃣ Handling Exceptions in Async Methods

By default, exceptions in @Async methods are lost if the return type is void. To handle exceptions properly, return a CompletableFuture<T> or log the error.

📌 Example: Handling Exceptions in Async Methods

1. Async Service (TaskService.java)

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class TaskService {

    @Async
    public CompletableFuture<String> processTask() {
        try {
            Thread.sleep(2000);
            throw new RuntimeException("Something went wrong!");
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }
}

2. Controller (TaskController.java)

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping("/run")
    public CompletableFuture<String> runTask() {
        return taskService.processTask()
                .exceptionally(ex -> "Error: " + ex.getMessage());
    }
}

📌 GET Request (GET /api/tasks/run)
📌 Response (HTTP 200 OK):

Error: Something went wrong!

Using exceptionally() ensures that errors are handled properly.

5️⃣ Configuring Thread Pool for Async Execution

By default, Spring Boot uses a single-threaded executor for @Async methods. You can define a custom thread pool for better performance.

📌 Example: Configuring a Thread Pool for @Async Tasks

1. Define a Custom Thread Pool (AsyncConfig.java)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("AsyncThread-");
        executor.initialize();
        return executor;
    }
}

2. Use the Custom Executor in an Async Service

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Service
public class ReportService {

    @Async("asyncExecutor")
    public CompletableFuture<String> generateReport() {
        return CompletableFuture.completedFuture("Report Generated Successfully!");
    }
}

Now, Spring Boot will use the configured thread pool instead of the default one.

🎯 Summary: Best Practices for Using @Async

Always enable async execution using @EnableAsync.
Use CompletableFuture<T> instead of void for better error handling.
Configure a custom thread pool for scalability (ThreadPoolTaskExecutor).
Log exceptions in async methods (exceptionally() for error handling).
Use meaningful method names (fetchData(), sendEmail(), processTask()).

🚀 Following these best practices ensures efficient, non-blocking execution in Spring Boot!

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare