java 78 lines · 3 tabs

Recurring Cleanup Job with Spring @Scheduled and a Shutdown-Aware Executor

Shared by codesnips Aug 2026
3 tabs
@Configuration
@EnableScheduling
@EnableConfigurationProperties(CleanupProperties.class)
public class SchedulingConfig {

    @Bean(destroyMethod = "shutdown")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(4);
        scheduler.setThreadNamePrefix("cleanup-sched-");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(30);
        scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return scheduler;
    }
}
3 files · java Explain with highlit

This snippet shows how a Spring Boot application runs a periodic maintenance task safely, without leaking threads or truncating in-flight work when the process stops. The core idea is to separate three concerns: the schedule trigger, the thread pool that actually runs the work, and the business logic itself.

In SchedulingConfig, @EnableScheduling turns on Spring's scheduling infrastructure, and a dedicated ThreadPoolTaskScheduler bean replaces the default single-threaded scheduler. Configuring it explicitly matters: the default @Scheduled executor is a one-thread pool, so a slow job can starve every other scheduled task. Setting setPoolSize gives headroom, and setThreadNamePrefix makes stack traces and logs legible. The two shutdown settings are the heart of the pattern — setWaitForTasksToCompleteOnShutdown(true) tells the pool to stop accepting new work but let running tasks finish, and setAwaitTerminationSeconds(30) bounds that wait so a stuck task cannot block JVM exit forever. Spring calls shutdown() on the bean automatically during context close because it implements DisposableBean.

CleanupProperties binds cleanup.* keys from configuration into a typed record-like holder, keeping the cron expression and batch size out of the code. Externalizing the cron string means the cadence can change per environment without a rebuild.

ExpiredTokenCleanupJob is the actual task. The @Scheduled(cron = ...) annotation references the property via SpEL, and zone pins interpretation to UTC so daylight-saving shifts never move the run time. The method itself is deliberately defensive: an AtomicBoolean guard (running) prevents overlapping executions if one run outlasts the interval, since @Scheduled on a fixed schedule does not skip a tick that arrives while the previous one is still busy. Work is done in bounded batches with deleteExpiredBatch, and the loop checks Thread.currentThread().isInterrupted() so that when the executor requests shutdown mid-run the job stops at a batch boundary rather than being killed halfway through a delete.

The trade-off is that graceful shutdown adds latency to redeploys, capped by awaitTerminationSeconds. This approach fits recurring cleanup, cache eviction, or aggregation jobs where correctness under shutdown matters more than instant termination; for heavier or distributed workloads a real job queue would be the better tool.


Related snips

Share this code

Here's the card — post it anywhere.

Recurring Cleanup Job with Spring @Scheduled and a Shutdown-Aware Executor — share card
Link copied