DEV Community

realNameHidden
realNameHidden

Posted on

How Does @Async Work Internally in Spring Boot?

Introduction ๐Ÿš€

Have you ever called a REST API and thought:

โ€œWhy is this request blocking until everything finishes?โ€

Now imagine sending an email, generating a report, or calling a slow third-party service โ€” do you really want your user to wait?

Thatโ€™s where @Async in Spring Boot comes to the rescue.

In simple terms, @Async lets Spring say:

โ€œYou go ahead, Iโ€™ll handle this task in the background.โ€

Behind this simple annotation, Spring Boot uses proxies, thread pools, and executors to run your code asynchronously โ€” and understanding how it works internally helps you avoid production bugs and interview traps.

In this blog, youโ€™ll learn:

  • What @Async really does behind the scenes
  • How Spring Boot executes async methods
  • Two complete, end-to-end Java 21 examples
  • Common mistakes and best practices

Core Concepts ๐Ÿง 

What Is @Async in Spring Boot?

@Async is a Spring annotation that allows a method to:

  • Run in a separate thread
  • Return immediately to the caller
  • Execute logic asynchronously using a TaskExecutor

Think of it like ordering food online ๐Ÿ”:

  • You place the order (API call)
  • The restaurant prepares it in the background
  • You donโ€™t stand at the counter waiting

How @Async Works Internally (Simple Explanation)

Internally, Spring Boot uses Spring AOP (proxy-based mechanism).

Hereโ€™s what really happens:

  1. Spring creates a proxy for your bean
  2. When an @Async method is called:
  • The proxy intercepts the call
  • Submits the method to a TaskExecutor
    1. The executor runs the method in a separate thread
    2. The main thread continues immediately

๐Ÿ“Œ Key takeaway:
@Async works only when the method is called from another Spring-managed bean.


Use Cases & Benefits

โœ… Best use cases:

  • Sending emails
  • Calling slow external APIs
  • Background processing
  • Event handling
  • Audit logging

๐ŸŽฏ Benefits:

  • Faster API responses
  • Better user experience
  • Cleaner separation of concerns

End-to-End Setup (Java 21 + Spring Boot) โš™๏ธ

Weโ€™ll build:

  • A REST API
  • An async service
  • A custom thread pool
  • cURL request + response

Example 1: Basic @Async Execution

1๏ธโƒฃ Enable Async Support

@Configuration
@EnableAsync
public class AsyncConfig {
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ This tells Spring to look for @Async methods.


2๏ธโƒฃ Async Service

@Service
public class NotificationService {

    @Async
    public void sendNotification() throws InterruptedException {
        // Simulate long-running task
        Thread.sleep(3000);
        System.out.println("Notification sent by thread: " + Thread.currentThread().getName());
    }
}
Enter fullscreen mode Exit fullscreen mode

3๏ธโƒฃ REST Controller

@RestController
@RequestMapping("/api")
public class NotificationController {

    private final NotificationService service;

    public NotificationController(NotificationService service) {
        this.service = service;
    }

    @GetMapping("/notify")
    public String triggerNotification() throws InterruptedException {
        service.sendNotification();
        return "Request accepted. Processing asynchronously.";
    }
}
Enter fullscreen mode Exit fullscreen mode

4๏ธโƒฃ cURL Request

curl -X GET http://localhost:8080/api/notify
Enter fullscreen mode Exit fullscreen mode

โœ… Response

Request accepted. Processing asynchronously.
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ The API returns immediately, while the task runs in the background.


Example 2: @Async with CompletableFuture (Recommended)

Why use this?

  • Better control
  • Non-blocking result handling
  • Cleaner async composition

1๏ธโƒฃ Async Service with Return Value

@Service
public class ReportService {

    @Async
    public CompletableFuture<String> generateReport() throws InterruptedException {
        Thread.sleep(2000);
        return CompletableFuture.completedFuture("Report generated successfully");
    }
}
Enter fullscreen mode Exit fullscreen mode

2๏ธโƒฃ REST Controller

@RestController
@RequestMapping("/api")
public class ReportController {

    private final ReportService service;

    public ReportController(ReportService service) {
        this.service = service;
    }

    @GetMapping("/report")
    public CompletableFuture<String> generate() throws InterruptedException {
        return service.generateReport();
    }
}
Enter fullscreen mode Exit fullscreen mode

3๏ธโƒฃ cURL Request

curl -X GET http://localhost:8080/api/report
Enter fullscreen mode Exit fullscreen mode

โœ… Response

Report generated successfully
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ The request thread is released early, while the task runs asynchronously.


Custom Thread Pool (Highly Recommended) ๐Ÿงต

@Configuration
@EnableAsync
public class AsyncExecutorConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-worker-");
        executor.initialize();
        return executor;
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Œ Without this, Spring uses a SimpleAsyncTaskExecutor, which is not production-friendly.


Best Practices โœ…

  1. Never call @Async inside the same class
  • Self-invocation bypasses Spring proxies
  1. Always define a custom executor
  • Avoid unbounded thread creation
  1. Use CompletableFuture for return values
  • Better async handling and composition
  1. Handle exceptions explicitly
  • Async exceptions wonโ€™t propagate automatically
  1. Keep async logic lightweight
  • @Async is not a replacement for message queues

Common Mistakes โŒ

  • โŒ Forgetting @EnableAsync
  • โŒ Expecting async behavior on private methods
  • โŒ Blocking async threads with heavy logic
  • โŒ Using @Async for CPU-heavy tasks
  • โŒ Assuming transactions propagate automatically

Conclusion ๐Ÿงฉ

@Async in Spring Boot looks simple, but internally it relies on:

  • Spring AOP proxies
  • Task executors
  • Thread pools

Once you understand how it works internally, you can:

  • Avoid subtle bugs
  • Write scalable applications
  • Impress interviewers ๐Ÿ˜‰

Call to Action ๐Ÿ“ฃ

๐Ÿ’ฌ Have questions about @Async or async bugs youโ€™ve faced?
๐Ÿง  Comment below โ€” letโ€™s discuss!
โญ Follow for more Java programming and Spring Boot internals content


Top comments (0)