Inside the Magic of @Async in Spring Boot — Complete Guide with Example
- shivaji191295
- Nov 9
- 5 min read
Learn how asynchronous processing works internally in Spring Boot, how threads are managed behind the scenes, and how to use @Async efficiently with real-time examples and visuals.
“This will make my method run in the background, send a dummy response to the UI, and continue later.”
📖 Introduction
When you build modern APIs, users expect them to be fast and responsive — even when performing heavy or time-consuming operations.Imagine an API that sends emails, generates PDF reports, or calls an Aadhaar validation service.
If you do this synchronously, the user waits until the entire task completes.But Spring Boot gives you a superpower — the @Async annotation — to make these operations asynchronous, freeing up your main thread immediately.
In this article, we’ll take a complete deep dive into how @Async works internally in Spring Boot — with code, thread behavior, proxy mechanism, configurations, and real-time execution flow.
What Is @Async in Spring Boot?
@Async is a Spring annotation used to run a method in a separate thread, allowing the main thread to continue processing without waiting for that method to complete.
In simpler words:
“Run this method in the background — I’ll continue my work.”
It’s ideal for:
Sending notifications or emails
Uploading files
Making external service calls
Generating reports
Processing background jobs
Example — Email Sending Service
Let’s take a simple email sending service example.This is the reference scenario we’ll use to explain every internal concept later.
// EmailService.java
@Service
public class EmailService {
@Async
public void sendEmail(String recipient) {
System.out.println("Sending email to " + recipient
+ " | Thread: " + Thread.currentThread().getName());
try {
Thread.sleep(3000); // simulate delay
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Email sent to " + recipient);
}
}// NotificationController.java
@RestController
@RequestMapping("/api")
public class NotificationController {
private final EmailService emailService;
public NotificationController(EmailService emailService) {
this.emailService = emailService;
}
@GetMapping("/sendEmail")
public String sendEmail(@RequestParam String recipient) {
emailService.sendEmail(recipient);
return "Request accepted — email will be sent shortly!";
}
}Output
Sending email to user@example.com | Thread: taskExecutor-1
Email sent to user@example.com✅ What’s happening
The controller returns the response immediately.
The email sending task runs in the background thread (taskExecutor-1).
The main servlet thread (http-nio-8080-exec-1) is free to serve other requests.
🔍 Internal Working of @Async — Step by Step
Let’s break down what happens inside Spring Boot when you call an @Async method.
Step 1 — Proxy Creation
When your Spring Boot application starts, the framework scans for beans that have methods annotated with @Async.
It then wraps those beans with a proxy (via AOP — Aspect-Oriented Programming).That proxy is what intercepts the method call.
So, when you call:
emailService.sendEmail("abc@example.com");You’re not calling the real method — you’re calling a proxy.
This proxy is responsible for submitting your method call to a background thread.
🧱 Important Rule:
If you call an @Async method from within the same class, Spring will not use the proxy — and the method will execute synchronously.
✅ Works (proxy involved)
emailService.sendEmail("abc@example.com");❌ Doesn’t work (no proxy)
this.sendEmail("abc@example.com");Step 2 — Thread Pool (Executor)
The proxy doesn’t run your code itself. It delegates execution to a TaskExecutor — a Spring abstraction for managing thread pools.
If you don’t configure one, Spring uses SimpleAsyncTaskExecutor by default, which creates a new thread for each task (not suitable for production).
Instead, define a ThreadPoolTaskExecutor with proper limits.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}Now, Spring submits all async tasks to this pool.
Step 3 — Execution Flow
Here’s the actual flow of events when you invoke the @Async method.

Controller executes in main servlet thread.
The call to @Async method is intercepted by proxy.
Proxy submits the method as a task to TaskExecutor.
Executor runs the task in a background thread.
Main servlet thread returns response immediately.
✅ Result: Non-blocking request, improved responsiveness.
Example — Using CompletableFuture
Sometimes you need to return a result asynchronously.
For example, generating a report or fetching data from a slow service.
@Service
public class ReportService {
@Async("taskExecutor")
public CompletableFuture<String> generateReport() throws InterruptedException {
System.out.println("Generating report on thread: " + Thread.currentThread().getName());
Thread.sleep(2000);
return CompletableFuture.completedFuture("Report Generated Successfully!");
}
}@RestController
public class ReportController {
private final ReportService reportService;
public ReportController(ReportService reportService) {
this.reportService = reportService;
} @GetMapping("/generateReport")
public CompletableFuture<String> generateReport() throws InterruptedException {
return reportService.generateReport();
}
}How It Works
The controller returns a CompletableFuture immediately.
Spring MVC keeps the HTTP request open (async mode).
Once the CompletableFuture completes, Spring automatically writes the response.
So, the UI still waits for the final result,but your main servlet thread is not blocked during that waiting time.
Internal Classes Involved
Class | Description |
AsyncAnnotationBeanPostProcessor | Detects @Async annotations and wraps beans with proxies. |
AsyncExecutionInterceptor | Intercepts method calls and delegates them to executor. |
TaskExecutor | Manages the thread pool. |
ThreadPoolTaskExecutor | Common implementation of TaskExecutor. |
CompletableFuture / AsyncResult | Represents async computation results. |
⚠️ Common Mistakes to Avoid
Mistake | Impact |
Calling async method within same class | No proxy → runs synchronously |
Missing @EnableAsync | Annotation ignored |
No custom executor | Creates too many unmanaged threads |
Returning void | Harder to handle exceptions |
Ignoring exceptions | Failures get lost silently |
Handling Exceptions in Async Methods
Spring provides a simple way to capture exceptions from async methods via AsyncConfigurer.
@Configuration
@EnableAsync
public class AsyncExceptionConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return new ThreadPoolTaskExecutor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
System.err.println("Async error in method: " + method.getName() + " - " + ex.getMessage());
}
}Thread Pool Behavior
Property | Description |
corePoolSize | Minimum number of threads kept alive |
maxPoolSize | Maximum concurrent threads |
queueCapacity | Queue size for pending tasks |
threadNamePrefix | Helps identify async threads in logs |
RejectedExecutionHandler | Action when queue is full (e.g., discard, run caller’s thread) |
Real-World Analogy
Think of @Async as a restaurant model:

You (controller) take orders.
You pass the order to the kitchen (proxy → executor).
The waiter (main thread) continues serving others.
The chef (background thread) cooks in parallel.
When the dish is ready, it’s served to the customer (callback/future completes).
That’s exactly how @Async works!
Best Practices
✅ Always define your own ThreadPoolTaskExecutor.
✅ Never call async methods from within the same class.
✅ Prefer returning CompletableFuture instead of void.
✅ Handle exceptions via AsyncUncaughtExceptionHandler.
✅ Monitor thread utilization in production.
Performance Benefits
Scenario | Blocking (No Async) | With @Async |
Threads used | 1 per request | 1 shared background thread |
API responsiveness | Waits until complete | Returns immediately |
Concurrency | Limited | Improved |
Scalability | Low | High |
Resource usage | Higher | Optimized |
Summary
Concept | Description |
@Async | Runs methods asynchronously in background thread |
Proxy | Intercepts method call and delegates to TaskExecutor |
TaskExecutor | Manages thread pool |
CompletableFuture | Represents async result |
Exception handling | Via AsyncConfigurer and handlers |
Benefit | Frees up main servlet thread for more requests |
Conclusion
@Async in Spring Boot is more than just an annotation —it’s a powerful asynchronous programming mechanism that uses Spring AOP, proxies, and executors under the hood.
By understanding its internals, you can:
Write efficient, non-blocking APIs
Manage thread pools safely
Improve scalability of your microservices
Remember:
Use @Async for parallelism, not for faster computation — it simply makes better use of system resources.


Comments