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.
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.csv
FileWorker-2 started processing image.png
data.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.