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
@Asyncreally 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:
- Spring creates a proxy for your bean
- When an
@Asyncmethod is called:
- The proxy intercepts the call
- Submits the method to a TaskExecutor
- The executor runs the method in a separate thread
- 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 {
}
📌 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());
}
}
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.";
}
}
4️⃣ cURL Request
curl -X GET http://localhost:8080/api/notify
✅ Response
Request accepted. Processing asynchronously.
📌 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");
}
}
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();
}
}
3️⃣ cURL Request
curl -X GET http://localhost:8080/api/report
✅ Response
Report generated successfully
📌 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;
}
}
📌 Without this, Spring uses a SimpleAsyncTaskExecutor, which is not production-friendly.
Best Practices ✅
- Never call
@Asyncinside the same class
- Self-invocation bypasses Spring proxies
- Always define a custom executor
- Avoid unbounded thread creation
- Use
CompletableFuturefor return values
- Better async handling and composition
- Handle exceptions explicitly
- Async exceptions won’t propagate automatically
- Keep async logic lightweight
-
@Asyncis not a replacement for message queues
Common Mistakes ❌
- ❌ Forgetting
@EnableAsync - ❌ Expecting async behavior on private methods
- ❌ Blocking async threads with heavy logic
- ❌ Using
@Asyncfor 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)