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)