Skip to main content

Command Palette

Search for a command to run...

The Dark Side of Asynchronous APIs in Spring Boot

Why @Async Isn't Always the Hero You Think It Is? Real-World Pitfalls, Performance Issues, and What to Watch Out For.

Updated
6 min read
The Dark Side of Asynchronous APIs in Spring Boot
H

Software Developer with 5+ years of experience in application development, proficient in Python, Java, ReactJS, and React Native. Successfully delivered several complex applications, demonstrating strong problem-solving and technical expertise.

Asynchronous programming has become a popular approach for improving the scalability and responsiveness of modern web applications. Spring Boot provides built-in support for asynchronous APIs using the @Async annotation, enabling developers to run time-consuming tasks in the background without blocking the main thread.

However, while async APIs are powerful, they’re not always the best solution. In fact, using them in the wrong context can lead to performance issues, unpredictable behavior, and difficult-to-maintain code.

In this article, we’ll explore the key drawbacks of asynchronous APIs in Spring Boot and when you might want to avoid using them.

1. Increased Code Complexity

Asynchronous code tends to be more difficult to read and maintain than its synchronous counterpart.

  • You often need to deal with CompletableFuture, callbacks, and non-linear control flow.

  • Adding error handling and chaining multiple asynchronous steps increases the cognitive load.

Example:

@Async
public CompletableFuture<String> processData() {
    return CompletableFuture.supplyAsync(() -> fetchData())
        .thenApplyAsync(data -> processData(data))
        .exceptionally(ex -> handleError(ex));
}

While powerful, this can quickly spiral into callback hell if not structured carefully.

2. Thread Pool and Resource Management

Spring Boot runs @Async methods using a thread pool. If not configured correctly, you risk:

  • Thread starvation (too many async tasks running, threads exhausted)

  • Blocked tasks (waiting indefinitely for available threads)

  • Increased memory usage (each async task holds memory while waiting)

Default Behavior:

If you don’t define a custom Executor, Spring uses a SimpleAsyncTaskExecutor, which:

  • Doesn’t reuse threads

  • Can lead to unbounded thread creation under load

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("AsyncExecutor-");
        executor.initialize();
        return executor;
    }
}

3. Difficulties in Transaction Management

Async methods run in separate threads, and thus outside the original transactional context.

Problem:

If you call an async method from a service method annotated with @Transactional, the async method won’t inherit that transaction.

Consequence:

  • Data may be read before it's committed

  • Rollbacks may not work as expected

  • Data inconsistencies may occur

4. Debugging and Error Handling are Harder

Debugging asynchronous code can be a nightmare:

  • Stack traces don’t show the full call path.

  • Exceptions thrown in async threads may be swallowed silently or lost.

  • Logs become harder to trace due to multiple threads.

Tip:

Use MDC for logging trace IDs across threads and monitor exceptions in CompletableFuture with .exceptionally() or .handle().

5. Harder Testing and Unit Coverage

Testing async methods is more complicated than synchronous ones.

  • You need to wait for the async task to complete.

  • Tests may pass or fail randomly if the async execution time varies.

Example:

@Test
public void testAsyncMethod() throws Exception {
    CompletableFuture<String> result = service.doAsyncWork();
    assertEquals("Done", result.get()); // Blocks until complete
}

For more fluent async testing, consider using tools like Awaitility.

6. Risk of Race Conditions and Out-of-Order Execution

Because async tasks run on different threads, they can execute out of order unless carefully coordinated. If two async tasks share data or depend on each other, you risk:

Out-of-Order Execution

  • Async tasks do not guarantee execution order unless explicitly coordinated (e.g., using .thenCompose, await, or synchronization primitives).

  • This can lead to logic errors if one task depends on the result of another that hasn't been completed yet.

Partial Results

  • If tasks fail or complete prematurely, you may end up with incomplete data or missing dependencies, especially in workflows that aggregate multiple async results.

Corrupted State

  • Shared mutable state accessed by multiple async tasks without proper synchronization can become corrupted, leading to bugs that are hard to reproduce or debug.

Deadlocks

  • Though less common in async code than in traditional multithreading, deadlocks can still occur, especially when tasks wait on each other in circular dependencies or when blocking calls are mixed with async flows.

7. Async Can Increase Latency in Some Scenarios

Async APIs introduce overhead for task scheduling, thread context switching, and handling future completion. In compute-intensive operations or tightly coupled workflows, this can actually worsen performance instead of improving it.

Real-World Scenario: When Async Broke Report Generation

In a recent project, I faced a situation where a report generation API in Spring Boot was taking around 5 minutes to complete. However, the API gateway in front of the service had a strict timeout limit of 1 minute, causing the request to fail for users even though the processing would eventually finish on the server.

The Plan:

  • To work around the gateway timeout and improve user experience, I decided to convert the report generation process into an asynchronous API using Spring’s @Async annotation. The goal was to offload the long-running task to a background thread and return a response immediately or use a polling mechanism for status.

What Went Wrong:

  • After implementing @Async, the report generation started failing intermittently, especially during one of the stages that fetched user data from the database. The system would throw an exception like "no data found", even though the data existed and the same logic worked in synchronous mode.

Diagnosis:

  • The problem stemmed from the async method running outside the original request and transaction context.

  • Because the operation spanned multiple dependent stages, involving several database reads, it was sensitive to transaction boundaries and thread-local context that was lost in async execution.

  • These failures only occurred under async execution and disappeared when running the same logic synchronously.

What Fixed It:

To resolve the issue:

  • I removed the @Async implementation from the API.

  • Instead, I moved the report generation logic to a Spring Boot Scheduler that runs the task at regular intervals.

  • This decoupled the long-running process from the API layer without sacrificing stability or data integrity.

Takeaway:

This scenario highlights why asynchronous APIs are not always the right choice, especially when:

  • The task involves multi-stage workflows with tightly coupled logic.

  • Database operations rely on a consistent transaction scope.

  • The goal is to bypass API timeouts, which may be better handled using background jobs, queues, or scheduled tasks instead.

When NOT to Use Async APIs

Avoid asynchronous APIs if:

  • The process involves multiple dependent stages

  • The operations must be performed in order

  • You're dealing with transaction-sensitive DB operations

  • You don't have control over the thread pool configuration

  • You're in a real-time or low-latency scenario

When Async APIs Make Sense

Use async APIs when:

  • You’re calling external services (e.g., REST APIs)

  • Doing parallelizable, independent tasks

  • Sending notifications, emails, or other non-blocking tasks

  • Handling file uploads, image processing, or background jobs

Conclusion

Asynchronous APIs in Spring Boot are a powerful tool, but not a universal solution. They introduce complexity, require careful resource management, and can cause subtle bugs if used incorrectly.

Before reaching for @Async, ask:

  • Is the task truly independent?

  • Can it benefit from concurrency?

  • Am I managing thread pools and exceptions properly?

In many cases, simplicity beats cleverness. If synchronous code works well and is easier to maintain, stick with it.

References

Asynchronous is a tool, not a shortcut. Use it where it fits, not where it breaks.

S

Hi,

image.png

this is spring boot jpa project, for jwt authentication, token works for post, get but not fordelete. Can you help me