Spring Boot Asynchronous Programming
Why Asynchronous Programming
- Increased Throughput: Non-blocking main thread handles more requests
- Improved Response Time: Time-consuming operations run in background
- Resource Utilization: Leverage multi-core CPU capabilities
- Decoupled Operations: Non-core operations like messaging and logging
Approach 1: @Async Annotation (Most Common)
1. Enable Async Support
java@Configuration @EnableAsync public class AsyncConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; } @Bean("mailExecutor") public Executor mailExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(5); executor.setQueueCapacity(50); executor.setThreadNamePrefix("mail-task-"); executor.initialize(); return executor; } }
2. Using @Async Annotation
java@Service @Slf4j public class AsyncService { @Async public void sendNotification(String userId, String message) { log.info("Sending notification to user: {}, thread: {}", userId, Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } log.info("Notification sent: {}", userId); } @Async("mailExecutor") public void sendEmail(String to, String subject, String content) { log.info("Sending email to: {}, thread: {}", to, Thread.currentThread().getName()); } @Async public CompletableFuture<String> processDataAsync(String data) { log.info("Processing data async: {}, thread: {}", data, Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return CompletableFuture.completedFuture("Result: " + data); } }
3. Calling Async Methods
java@RestController @RequestMapping("/async") @RequiredArgsConstructor public class AsyncController { private final AsyncService asyncService; @GetMapping("/notify") public Result<Void> sendNotification(@RequestParam String userId) { asyncService.sendNotification(userId, "You have a new message"); return Result.success("Notification sending"); } @GetMapping("/process") public Result<String> processData(@RequestParam String data) throws Exception { CompletableFuture<String> future = asyncService.processDataAsync(data); String result = future.get(5, TimeUnit.SECONDS); return Result.success(result); } @GetMapping("/batch") public Result<List<Integer>> batchProcess() throws Exception { List<CompletableFuture<Integer>> futures = IntStream.range(1, 11) .mapToObj(asyncService::calculateAsync) .collect(Collectors.toList()); CompletableFuture<Void> allFutures = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0]) ); allFutures.join(); List<Integer> results = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); return Result.success(results); } }
Approach 2: CompletableFuture (Java 8+)
java@Service @Slf4j public class CompletableFutureService { private final ExecutorService executor = Executors.newFixedThreadPool(10); public CompletableFuture<String> combineAsyncTasks() { CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> { log.info("Executing task 1"); sleep(1000); return "Result 1"; }, executor); CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> { log.info("Executing task 2"); sleep(1500); return "Result 2"; }, executor); return task1.thenCombine(task2, (result1, result2) -> { return result1 + " + " + result2; }); } public CompletableFuture<String> taskChain(String input) { return CompletableFuture.supplyAsync(() -> { log.info("Step 1: Process input"); return input.toUpperCase(); }, executor) .thenApplyAsync(result -> { log.info("Step 2: Add prefix"); return "PREFIX_" + result; }, executor) .thenApplyAsync(result -> { log.info("Step 3: Add suffix"); return result + "_SUFFIX"; }, executor) .exceptionally(ex -> { log.error("Task chain exception: {}", ex.getMessage()); return "DEFAULT_VALUE"; }); } public String asyncWithTimeout() { try { return CompletableFuture.supplyAsync(() -> { sleep(5000); return "Result"; }, executor) .orTimeout(3, TimeUnit.SECONDS) .exceptionally(ex -> "Timeout default") .get(); } catch (Exception e) { return "Exception: " + e.getMessage(); } } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
Approach 3: Spring WebFlux (Reactive)
java@RestController @RequestMapping("/reactive") public class ReactiveController { @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamData() { return Flux.interval(Duration.ofSeconds(1)) .map(seq -> "Data " + seq + " at " + LocalTime.now()) .take(10); } @GetMapping("/mono/{id}") public Mono<ResponseEntity<User>> getUser(@PathVariable Long id) { return Mono.fromCallable(() -> findUserById(id)) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); } @PostMapping("/users") public Mono<User> createUser(@RequestBody Mono<User> userMono) { return userMono.flatMap(this::saveUser); } }
Approach 4: DeferredResult (Servlet Async)
java@RestController @RequestMapping("/deferred") public class DeferredResultController { @GetMapping("/task") public DeferredResult<String> asyncTask() { DeferredResult<String> deferredResult = new DeferredResult<>(10000L, "Timeout"); new Thread(() -> { try { Thread.sleep(3000); deferredResult.setResult("Task completed"); } catch (InterruptedException e) { deferredResult.setErrorResult("Task interrupted"); } }).start(); deferredResult.onTimeout(() -> { System.out.println("Task timeout"); }); deferredResult.onCompletion(() -> { System.out.println("Task completion callback"); }); return deferredResult; } @GetMapping("/callable") public Callable<String> callableTask() { return () -> { Thread.sleep(3000); return "Callable task completed"; }; } }
Async Exception Handling
java@Configuration @Slf4j public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { log.error("Async method exception - Method: {}, Params: {}", method.getName(), params, ex); sendAlert(method.getName(), ex.getMessage()); } private void sendAlert(String methodName, String errorMessage) { log.warn("Sending alert: Method {} failed, error: {}", methodName, errorMessage); } } @Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Override @Bean(name = "taskExecutor") public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setThreadNamePrefix("async-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncExceptionHandler(); } }
Best Practices
1. Thread Pool Configuration
java@Bean("ioIntensiveExecutor") public Executor ioIntensiveExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2); executor.setMaxPoolSize(100); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("io-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Bean("cpuIntensiveExecutor") public Executor cpuIntensiveExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1); executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() + 1); executor.setQueueCapacity(100); executor.setThreadNamePrefix("cpu-task-"); executor.initialize(); return executor; }
2. Avoid Async Pitfalls
java@Service @Slf4j public class AsyncPitfallService { // ❌ Wrong: Calling @Async method in same class won't be async public void wrongAsyncCall() { this.asyncMethod(); // Won't go through proxy } @Async public void asyncMethod() { // Async logic } // ✅ Correct: Call through injected proxy @Autowired private AsyncPitfallService self; public void correctAsyncCall() { self.asyncMethod(); // Goes through proxy } }
Summary
| Approach | Use Case | Pros | Cons |
|---|---|---|---|
| @Async | Simple async tasks | Easy to use | Limited features |
| CompletableFuture | Complex async orchestration | Powerful, composable | Learning curve |
| WebFlux | High-concurrency reactive | Non-blocking, high performance | Different programming model |
| DeferredResult | Servlet async | Compatible with traditional MVC | Lower level |
Recommendations:
- Simple background tasks: @Async
- Complex async workflows: CompletableFuture
- High-concurrency APIs: WebFlux
- Traditional project upgrade: DeferredResult