Home/Learn/Spring Boot/Async Processing with @Async

Async Processing with @Async

Intermediate
Advanced

@Async runs a method in a background thread from a configurable TaskExecutor; combine with CompletableFuture to compose results without blocking the caller.

Overview

@Async is Spring's mechanism for running a method in a background thread without the caller blocking. When you annotate a Spring-managed method with @Async, the proxy intercepts the call and submits it to a TaskExecutor (thread pool) instead of executing it in the caller's thread. The caller gets back immediately — the method runs concurrently. This is ideal for fire-and-forget side effects (sending emails, audit logging, async notifications) and for fan-out patterns where you want to fire multiple independent operations in parallel and join their results. @Async requires @EnableAsync on a @Configuration class and the method must be on a different bean from the caller (same self-invocation limitation as @Transactional).

Basic @Async — Fire-and-Forget

For fire-and-forget tasks (no result needed), annotate the method with @Async and return void. Add @EnableAsync to a configuration class. Spring uses SimpleAsyncTaskExecutor by default — this creates a new thread per call with no pooling, which is fine for development but not for production. Always configure a ThreadPoolTaskExecutor for production.

Java — @Async Fire-and-Forget
// 1. Enable async processing
@SpringBootApplication
@EnableAsync
public class App { }

// 2. Async method — runs in background thread, caller does not wait
@Service
public class NotificationService {

    @Async   // executes in background thread
    public void sendOrderConfirmation(String email, String orderId) {
        // Caller returns immediately; this runs concurrently
        emailClient.send(email, "Order confirmed: " + orderId);
        log.info("Sent confirmation to {} on thread {}", email,
                 Thread.currentThread().getName());
    }
}

// 3. Caller — does not block
@Service
@RequiredArgsConstructor
public class OrderService {
    private final NotificationService notificationService;

    @Transactional
    public Order placeOrder(OrderRequest req) {
        Order order = orderRepo.save(new Order(req));
        // Returns immediately — email is sent asynchronously
        notificationService.sendOrderConfirmation(req.getEmail(), order.getId().toString());
        return order;
    }
}

CompletableFuture — Async with Return Value

When you need the result, return CompletableFuture<T>. The caller can then compose multiple async calls in parallel with CompletableFuture.allOf() and join results — a powerful fan-out pattern for aggregating data from multiple services.

Java — Async Fan-Out with CompletableFuture
@Service
public class ProductEnrichmentService {

    @Async
    public CompletableFuture<Stock> fetchStock(Long productId) {
        Stock stock = inventoryClient.getStock(productId);  // slow external call
        return CompletableFuture.completedFuture(stock);
    }

    @Async
    public CompletableFuture<PriceInfo> fetchPrice(Long productId) {
        PriceInfo price = pricingClient.getPrice(productId);
        return CompletableFuture.completedFuture(price);
    }
}

@Service
@RequiredArgsConstructor
public class ProductDetailService {
    private final ProductEnrichmentService enrichmentService;

    // Fan-out: fire both calls in parallel, join results
    public ProductDetail getDetail(Long productId) throws Exception {
        CompletableFuture<Stock>     stockFuture = enrichmentService.fetchStock(productId);
        CompletableFuture<PriceInfo> priceFuture = enrichmentService.fetchPrice(productId);

        // Wait for both to complete
        CompletableFuture.allOf(stockFuture, priceFuture).join();

        // Sequential: 500ms + 300ms = 800ms
        // Parallel:   max(500ms, 300ms) = 500ms  ← 37% faster
        return ProductDetail.of(stockFuture.get(), priceFuture.get());
    }
}

ThreadPoolTaskExecutor — Production Configuration

The default SimpleAsyncTaskExecutor creates a new thread per call — no pooling, no backpressure. For production, configure a ThreadPoolTaskExecutor with appropriate core/max pool sizes and a bounded queue. Spring Boot auto-configuration provides a default ThreadPoolTaskExecutor bean (spring.task.execution.pool.*) when @EnableAsync is active.

YAML + Java — ThreadPoolTaskExecutor
# application.yml — Spring Boot auto-configured thread pool
spring:
  task:
    execution:
      pool:
        core-size: 5          # threads always kept alive
        max-size: 20          # max concurrent threads
        queue-capacity: 100   # tasks queued before rejecting
        keep-alive: 60s       # idle threads above core-size survive for 60s
      thread-name-prefix: async-

# Or configure programmatically for full control:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
        exec.setCorePoolSize(5);
        exec.setMaxPoolSize(20);
        exec.setQueueCapacity(100);
        exec.setThreadNamePrefix("async-");
        exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // fallback
        exec.initialize();
        return exec;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) ->
            log.error("Unhandled exception in @Async method {}", method.getName(), ex);
    }
}

Key Points to Remember

  • 1@EnableAsync on a @Configuration class activates @Async support; without it, @Async annotations are silently ignored.
  • 2@Async works via AOP proxy — self-invocation (calling an @Async method on the same bean) bypasses the proxy and runs synchronously.
  • 3Return CompletableFuture<T> for async methods that produce results; combine with CompletableFuture.allOf() for parallel fan-out.
  • 4Never use the default SimpleAsyncTaskExecutor in production — configure a ThreadPoolTaskExecutor with bounded pool and queue.
  • 5Exceptions thrown in void @Async methods are silently swallowed; configure AsyncUncaughtExceptionHandler to log or alert.
  • 6@Transactional and @Async on the same method is a trap — the transaction commits before the method returns, and the async work runs outside it.

Interview Questions

Sign in to ask Aria
1

How does @Async work in Spring Boot and what does @EnableAsync do?

EasyInfosys
2

Why does @Async not work when calling the method from the same bean?

MediumAmazon
3

How would you execute two independent external API calls in parallel and combine their results?

MediumFlipkart
4

What happens to exceptions thrown in a void @Async method?

MediumUber
5

What are the risks of combining @Async and @Transactional on the same method?

HardGoldman Sachs

Ask Aria about Async Processing with @Async

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…