Spring
Async
REST API
Interfaces
Troubleshooting

Async not working in Spring API rest with Interfaces

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When @Async appears to run synchronously in a Spring REST application, the cause is usually proxy behavior, not thread scheduling bugs. This is especially confusing when interfaces are involved, because method interception rules depend on how beans are proxied and invoked. A reliable fix starts with understanding exactly how Spring applies @Async.

How Spring @Async Actually Works

@Async is implemented through proxy interception. A call must go through a Spring managed proxy for asynchronous execution to occur.

That means three prerequisites:

  • async support is enabled with @EnableAsync,
  • the target method is called through an injected bean reference,
  • the proxy can intercept the method.

If any one of those is missing, the call runs on the caller thread.

Correct Baseline Configuration

Start with explicit async configuration and an executor bean.

java
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.scheduling.annotation.EnableAsync;
4import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
5
6import java.util.concurrent.Executor;
7
8@Configuration
9@EnableAsync
10public class AsyncConfig {
11
12    @Bean(name = "appExecutor")
13    public Executor appExecutor() {
14        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
15        executor.setCorePoolSize(4);
16        executor.setMaxPoolSize(8);
17        executor.setQueueCapacity(100);
18        executor.setThreadNamePrefix("app-async-");
19        executor.initialize();
20        return executor;
21    }
22}

Service interface and implementation:

java
1import java.util.concurrent.CompletableFuture;
2
3public interface ReportService {
4    CompletableFuture<String> generateReport();
5}
java
1import org.springframework.scheduling.annotation.Async;
2import org.springframework.stereotype.Service;
3
4import java.util.concurrent.CompletableFuture;
5
6@Service
7public class ReportServiceImpl implements ReportService {
8
9    @Override
10    @Async("appExecutor")
11    public CompletableFuture<String> generateReport() {
12        String thread = Thread.currentThread().getName();
13        return CompletableFuture.completedFuture("running on " + thread);
14    }
15}

Controller calling through injected interface:

java
1import org.springframework.web.bind.annotation.GetMapping;
2import org.springframework.web.bind.annotation.RestController;
3
4import java.util.concurrent.CompletableFuture;
5
6@RestController
7public class ReportController {
8    private final ReportService reportService;
9
10    public ReportController(ReportService reportService) {
11        this.reportService = reportService;
12    }
13
14    @GetMapping("/reports/async")
15    public CompletableFuture<String> asyncReport() {
16        return reportService.generateReport();
17    }
18}

Why It Fails In Real Projects

Self invocation

If a method in ReportServiceImpl calls another @Async method in the same class, that call bypasses proxy interception. It executes synchronously.

Method visibility and proxy mode

Proxy interception works best for public methods. Private methods or final methods can block proxy behavior depending on proxy strategy.

Missing @EnableAsync

Without global async enablement, @Async annotations are ignored.

Wrong return expectations

If you return a plain value type and then immediately wait for it in the controller, you can accidentally remove async benefits.

Patterns To Avoid Self Invocation Issues

Split orchestration and async work into separate beans.

  • Bean A receives HTTP request.
  • Bean A calls Bean B async method through injected dependency.
  • Bean B owns background logic.

This guarantees call path goes through proxy and keeps responsibilities clear.

Error Handling And Observability

Unhandled exceptions in async methods can be easy to miss. For CompletableFuture, attach error handling stages. For void async methods, configure AsyncUncaughtExceptionHandler.

Also log thread names in test and dev environments. If all logs show request thread names, proxy interception is still not happening.

Testing The Behavior

A quick integration test should assert that returned thread name starts with your executor prefix. That confirms the method did not execute on the caller thread.

You can also assert that controller response returns before a long async operation completes, depending on API design.

Common Pitfalls

  • Calling @Async methods from within the same bean instance.
  • Forgetting @EnableAsync in configuration.
  • Not declaring a dedicated executor and relying on defaults without understanding limits.
  • Using non public methods for async work and expecting interception.
  • Blocking immediately on async result and losing non blocking benefit.

Summary

  • Spring @Async depends on proxy interception, not magic thread switching.
  • Calls must pass through an injected Spring bean reference.
  • Interfaces are fine, but self invocation still bypasses proxies.
  • Use explicit executor configuration and thread name verification.
  • Split beans and add tests to ensure async behavior is truly active.

Course illustration
Course illustration

All Rights Reserved.