Learn when to avoid using @async in Spring applications. Understand pitfalls, best practices, and real-world examples in Java programming.
📌 Introduction
Imagine you’re ordering food at a busy restaurant. Instead of waiting for your order, you tell the waiter, “Just bring it whenever—it’s not urgent.” Sounds convenient, right?
That’s exactly what @Async in Spring does—it lets tasks run in the background so your main flow doesn’t wait.
But here’s the catch: not every task should be asynchronous.
Using @Async blindly can lead to bugs, performance issues, and confusing behavior. In this blog, we’ll break down when you should avoid using @Async in Spring applications so you can write cleaner, safer Java code.
🧠 Core Concepts
🔹 What is @Async?
In Spring Framework, @Async allows methods to run in a separate thread, enabling non-blocking execution.
Think of it like:
Sending an email in the background while continuing your work.
✅ When @Async is Useful
- Sending emails
- Logging
- Calling external APIs
- Background processing
❌ When You Should Avoid Using @Async
1. ❗ When You Need Immediate Results
If your method returns data needed immediately, @Async is a bad choice.
👉 Example: Fetching user details for login
2. ❗ Transactional Boundaries (@Transactional)
@Async runs in a different thread, so it does NOT share the same transaction context.
👉 This can lead to:
- Partial commits
- Data inconsistency
3. ❗ Calling Internal Methods (Same Class)
Spring uses proxies. Calling an @Async method inside the same class won’t work asynchronously.
4. ❗ Error Handling is Critical
Exceptions in async methods are not propagated normally.
👉 You may miss critical failures.
5. ❗ High Volume Without Thread Management
Uncontrolled async calls = thread exhaustion = app crash.
⚖️ Benefits (When Used Correctly)
- Improves performance
- Non-blocking operations
- Better user experience
💻 Code Examples
❌ Example 1: Incorrect Use of @Async (Avoid This)
package com.example.demo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// ❌ BAD: Async used for critical data retrieval
@Async
public String getUserName() {
// Simulate delay
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "User123";
}
}
package com.example.demo.controller;
import com.example.demo.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/user")
public String getUser() {
// ❌ Problem: This will NOT return the expected result immediately
return userService.getUserName().toString();
}
}
🔴 Issue:
-
@AsyncreturnsFuture/CompletableFuture, not direct value - Controller may behave unexpectedly
✅ Example 2: Correct Use with Proper Async Handling
package com.example.demo.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class NotificationService {
// ✅ GOOD: Background task (email simulation)
@Async
public CompletableFuture<String> sendEmailNotification() {
try {
Thread.sleep(2000); // Simulate delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture("Email sent successfully!");
}
}
package com.example.demo.controller;
import com.example.demo.service.NotificationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
public class NotificationController {
private final NotificationService notificationService;
public NotificationController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/notify")
public CompletableFuture<String> notifyUser() {
// ✅ Non-blocking response
return notificationService.sendEmailNotification();
}
}
▶️ CURL Request
curl -X GET http://localhost:8080/notify
✅ Response
"Email sent successfully!"
🔗 Useful Resources
- Spring Framework Documentation: https://docs.spring.io/spring-framework/reference/integration/scheduling.html
- Oracle Java Docs: https://docs.oracle.com/en/java/
✅ Best Practices
- ✔️ Use
@Asynconly for non-critical background tasks - ✔️ Always return
CompletableFuturefor better handling - ✔️ Configure a custom thread pool (avoid default)
- ✔️ Avoid using
@Asyncwith@Transactional - ✔️ Never call
@Asyncmethods internally (same class)
🏁 Conclusion
Using @Async in Spring applications can significantly improve performance—but only when used wisely.
Now you know when to avoid using @Async in Spring applications, especially in cases involving transactions, immediate responses, or internal method calls.
Think of @Async as a powerful tool—not a default choice.
🚀 Call to Action
Have you faced issues with @Async in your projects? Drop your questions or experiences in the comments—I’d love to help you out!
Top comments (0)