Java
Spring Framework
Asynchronous Programming
Task Scheduling
Concurrency

Asynchronous task within scheduler in Java Spring

Master System Design with Codemia

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

Introduction

In Spring, scheduling and asynchronous execution are two separate features that can be combined. Scheduling decides when a method is triggered. @Async decides which thread does the work. When used together, the scheduled trigger can hand work off to another executor thread so that long-running jobs do not block the scheduler thread.

Enable Both Features

To combine scheduling and async execution, enable both systems.

java
1import org.springframework.context.annotation.Configuration;
2import org.springframework.scheduling.annotation.EnableAsync;
3import org.springframework.scheduling.annotation.EnableScheduling;
4
5@Configuration
6@EnableScheduling
7@EnableAsync
8public class SchedulingConfig {
9}

Without @EnableScheduling, your @Scheduled methods never run. Without @EnableAsync, @Async does nothing.

The Basic Pattern

The simplest combination is a scheduled method that is also asynchronous.

java
1import org.springframework.scheduling.annotation.Async;
2import org.springframework.scheduling.annotation.Scheduled;
3import org.springframework.stereotype.Service;
4
5@Service
6public class ReportJob {
7
8    @Async
9    @Scheduled(fixedDelay = 5000)
10    public void generateReport() throws InterruptedException {
11        System.out.println("Started on thread: " + Thread.currentThread().getName());
12        Thread.sleep(8000);
13        System.out.println("Finished on thread: " + Thread.currentThread().getName());
14    }
15}

This lets the scheduler trigger the method every five seconds while the actual work runs on an async executor thread.

Without @Async, the scheduler thread would block for the entire eight-second task duration.

Why This Matters

By default, scheduled work can behave effectively serially if the work takes longer than the scheduling interval and all execution stays on the scheduling thread. That leads to delayed triggers and confusing timing.

Using @Async can help when:

  • the scheduled job performs slow I/O
  • the work should overlap across runs
  • you do not want one long job to delay all others

But it also changes behavior. Overlapping executions can now happen, so concurrency must be intentional rather than accidental.

A Better Structure: Scheduled Trigger Calls Async Service

A clearer design is often to keep the scheduled trigger small and delegate the long-running work to a separate async method.

java
1import org.springframework.scheduling.annotation.Scheduled;
2import org.springframework.stereotype.Component;
3
4@Component
5public class JobScheduler {
6
7    private final ReportService reportService;
8
9    public JobScheduler(ReportService reportService) {
10        this.reportService = reportService;
11    }
12
13    @Scheduled(fixedRate = 10000)
14    public void scheduleReport() {
15        reportService.generateReportAsync();
16    }
17}
java
1import org.springframework.scheduling.annotation.Async;
2import org.springframework.stereotype.Service;
3
4@Service
5public class ReportService {
6
7    @Async
8    public void generateReportAsync() {
9        System.out.println("Async thread: " + Thread.currentThread().getName());
10    }
11}

This separation makes the control flow easier to test and reason about.

Configure a Real Executor

Relying on default async behavior is rarely ideal in production. Define an executor so thread counts and queueing are explicit.

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

Then target it:

java
1@Async("jobExecutor")
2public void generateReportAsync() {
3    // work
4}

That keeps background work under control instead of silently spawning behavior you did not plan for.

Self-Invocation Trap

A classic Spring pitfall is calling an @Async method from another method in the same class.

Bad example:

java
public void scheduleReport() {
    generateReportAsync();
}

If both methods are in the same bean, the call may bypass the Spring proxy, which means @Async is never applied.

That is why the separate scheduler bean and async service bean pattern is often safer. The call crosses a Spring-managed bean boundary, so the async proxy can do its work.

Think About Overlap and Idempotency

Once scheduling hands work to async threads, the next trigger may fire before the previous run finishes. That can be correct, or it can be disastrous, depending on the job.

If overlap is unsafe, you may need:

  • a lock
  • a distributed coordination mechanism
  • idempotent job logic
  • queue-based buffering instead of direct overlap

Asynchronous scheduling improves throughput only when the business logic is concurrency-safe.

Common Pitfalls

One common mistake is adding @Async but forgetting @EnableAsync, which leaves the method synchronous.

Another issue is self-invocation inside the same bean, which bypasses the async proxy and makes it look as if @Async is broken.

Developers also often forget that async scheduled work can overlap. That can lead to duplicate processing, database contention, or conflicting writes.

Finally, exceptions in async methods do not behave like exceptions in a normal synchronous call stack. Logging, monitoring, and explicit error handling become more important once work is offloaded to executor threads.

Summary

  • '@Scheduled controls when work starts, while @Async controls where it runs.'
  • Enable both features with @EnableScheduling and @EnableAsync.
  • A clean design is a small scheduled trigger that delegates to a separate async service.
  • Configure a dedicated executor for predictable behavior in production.
  • Be explicit about overlap, error handling, and self-invocation so async scheduling behaves the way you expect.

Course illustration
Course illustration

All Rights Reserved.