Introduction
In real-world applications, we often deal with tasks that take time — like sending emails, processing files, or generating reports.
Running these tasks in the main thread can slow down your API response time.
That’s where multithreading comes in — it helps execute long-running tasks in the background while freeing the main thread to handle user requests faster.
Scenario: File Upload Processing in Background
Let’s say we are building an API that allows users to upload a large file.
Instead of blocking the request until the file is completely processed, we’ll handle it asynchronously using @Async and a custom ThreadPoolTaskExecutor.
Step 1 — Create the Async Configuration
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // Minimum threads in the pool
executor.setMaxPoolSize(20); // Maximum threads allowed
executor.setQueueCapacity(50); // Tasks waiting in queue
executor.setThreadNamePrefix("FileWorker-"); // Thread name prefix
executor.initialize();
// Optional: Handle rejected tasks gracefully
executor.setRejectedExecutionHandler((r, e) -> {
System.out.println("Task rejected: " + r.toString());
});
return executor;
}
}
Explanation:
- @Configuration → Marks this class as a Spring configuration.
- @EnableAsync → Enables asynchronous method execution in your project.
- ThreadPoolTaskExecutor → Manages a pool of worker threads for executing tasks.
- RejectedExecutionHandler → Optional handler when the queue is full (e.g., log or retry logic).
Step 2 — Create the Service Class
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class FileProcessingService {
@Async("taskExecutor")
public void processFile(String fileName) {
System.out.println(Thread.currentThread().getName() + " started processing " + fileName);
try {
Thread.sleep(5000); // Simulate time-consuming task
System.out.println(fileName + " processed successfully!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("File processing interrupted: " + e.getMessage());
}
}
}Why @Async?
- Marks the method for asynchronous execution.
- Executes the task in a background thread instead of blocking the main thread.
- The "taskExecutor" refers to the bean defined in AsyncConfig.
Step 3 — Create the REST Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/files")
public class FileController {
@Autowired
private FileProcessingService fileService;
@PostMapping("/upload")
public String uploadFile(@RequestParam String fileName) {
fileService.processFile(fileName); // Executes asynchronously
return "File upload received. Processing in background...";
}
}What Happens Here?
- The controller accepts a request (e.g., /api/files/upload?fileName=data.csv).
- The main thread immediately responds with a success message.
- The file is processed in the background by another thread (FileWorker-*).
Real-World Use Cases
- Sending emails or notifications
- Generating PDF reports
- Image or video processing
- Large file import/export
- API call chaining in microservices
Why Use Custom Executor Instead of Default @Async?
If you just use @Async without defining a configuration, Spring uses a SimpleAsyncTaskExecutor, which:
- Does not reuse threads efficiently (creates new threads per task).
- Has no queue, no thread limits, and can cause performance issues in production.
A ThreadPoolTaskExecutor lets you:
- Limit thread count (corePoolSize, maxPoolSize).
- Handle overflow tasks (queueCapacity).
- Assign meaningful thread names.
- Log and manage rejected tasks safely.
Output Example
FileWorker-1 started processing data.csvFileWorker-2 started processing image.pngdata.csv processed successfully!image.png processed successfully!
Interview Tip
If asked in an interview:
“How do you handle background tasks in Spring Boot without blocking the main thread?”
Answer:
I use Spring’s @Async for asynchronous execution along with a custom ThreadPoolTaskExecutor defined in a configuration class.
This lets me control thread pool size, queue capacity, and thread naming for better scalability and debugging in production.
0 Comments