top of page
Search

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.


ree

  1. Controller executes in main servlet thread.

  2. The call to @Async method is intercepted by proxy.

  3. Proxy submits the method as a task to TaskExecutor.

  4. Executor runs the task in a background thread.

  5. 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:


ree

  • 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


bottom of page