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)