Spring Framework
Spring Async
Concurrency
Task Management
Java Development

Avoid performing Spring Async task twice at the same time

Master System Design with Codemia

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

Introduction

@Async in Spring makes it easy to run work on a background executor, but it does not automatically prevent the same task from being started twice. If the method can be triggered by multiple requests, schedulers, or events, duplicate execution is still possible unless you add explicit coordination.

The right solution depends on scope. If you only care about one JVM instance, an in-memory lock may be enough. If the application runs on several nodes, you need a shared coordination mechanism such as a database lock or a distributed lock.

Why @Async Alone Does Not Prevent Duplicates

An async method just means "submit this work to an executor":

java
1@Service
2public class ReportService {
3
4    @Async
5    public void generateNightlyReport() {
6        // expensive work
7    }
8}

If two callers invoke generateNightlyReport() at nearly the same time, Spring will happily submit both tasks. There is no built-in singleton-task guarantee here.

Use an In-Memory Guard for a Single Instance

If only one application instance exists, a simple atomic flag can work:

java
1import java.util.concurrent.atomic.AtomicBoolean;
2import org.springframework.scheduling.annotation.Async;
3import org.springframework.stereotype.Service;
4
5@Service
6public class ReportService {
7    private final AtomicBoolean running = new AtomicBoolean(false);
8
9    @Async
10    public void generateNightlyReport() {
11        if (!running.compareAndSet(false, true)) {
12            return;
13        }
14
15        try {
16            // do work
17        } finally {
18            running.set(false);
19        }
20    }
21}

This prevents overlap inside one JVM. It is simple and effective for one-node deployments, but it does nothing across multiple pods or servers.

Use a Lock That Matches Deployment Scope

In clustered deployments, you need coordination outside process memory. One common pattern is a lock row in a database or a distributed lock provider.

A conceptual database-lock flow looks like this:

java
1boolean acquired = lockRepository.tryAcquire("nightly-report");
2if (!acquired) {
3    return;
4}
5
6try {
7    runReport();
8} finally {
9    lockRepository.release("nightly-report");
10}

The exact implementation varies, but the principle is stable: the lock must live in a system shared by every application instance that could schedule the task.

Scheduler Example with Guarding

This issue often appears with scheduled jobs:

java
1import org.springframework.scheduling.annotation.Scheduled;
2import org.springframework.stereotype.Component;
3
4@Component
5public class ReportScheduler {
6    private final ReportService reportService;
7
8    public ReportScheduler(ReportService reportService) {
9        this.reportService = reportService;
10    }
11
12    @Scheduled(cron = "0 0 * * * *")
13    public void trigger() {
14        reportService.generateNightlyReport();
15    }
16}

If the job sometimes runs longer than the schedule interval, overlap becomes likely. That is exactly when a guard or lock is required.

Keep Failure Handling Safe

Whatever lock strategy you use, always release the lock in finally. Otherwise a thrown exception can leave the task permanently blocked.

You also need to decide what "already running" should mean operationally:

  • skip the new request
  • queue the new request
  • return an explicit status to the caller

That policy is a business decision, not just a concurrency detail.

Common Pitfalls

  • Assuming @Async prevents duplicate execution by itself.
  • Using an in-memory flag in a multi-instance deployment.
  • Forgetting to clear the running flag or release the lock on failure.
  • Scheduling work more often than the task can realistically finish.
  • Solving a business workflow problem with only thread-level thinking.

Summary

  • '@Async runs work in the background but does not prevent duplicate starts.'
  • For one JVM, an AtomicBoolean or similar in-memory guard can work.
  • For multiple instances, use a shared lock such as a database or distributed lock.
  • Always release the lock in a finally block.
  • Decide whether duplicate triggers should be skipped, queued, or reported explicitly.

Course illustration
Course illustration

All Rights Reserved.