Task scheduling is frequently used in web applications to execute different jobs at certain times without any manual input. Examples include backing up the database, sending newsletter emails, deleting log files, moving files from one server to another server, just to name a few.

Spring Boot provides multiple ways to schedule tasks. You can either use @Scheduled annotation or use a custom thread pool to run your tasks at specific times.

In this article, we will learn how to schedule tasks in a Spring Boot application using @Scheduled annotation. We will also take a look at using a custom thread pool to execute scheduled tasks.

Dependencies

We only need spring-boot-starter dependency to use the @Scheduled annotation. Add the following to your build.gradle file:

implementation 'org.springframework.boot:spring-boot-starter'

If you are using Maven, add the following dependency to your pom.xml file:

<dependency> 
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Enable Scheduling

We can enable the scheduling functionality by adding @EnableScheduling annotation to the main application class or any other configuration class like below:

Application.java

package com.attacomsian.scheduling;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

@EnableScheduling annotation ensures that a background task executor is created with single thread.

Scheduling Tasks

Scheduling a task is easy. Simply add the @Scheduled annotation to any method you want to schedule and set the time it should execute. However, all such methods must meet the following two conditions:

  1. The method return type should be void (void)
  2. The method should not accept any parameter

That being said, let us create a Java class that will act as a container to hold all our scheduled tasks:

ScheduledTasks.java

package com.attacomsian.scheduling;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.format.DateTimeFormatter;

@Component
public class ScheduledTasks {

    private static final Logger logger = LoggerFactory.getLogger(ScheduledTasks.class);
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    // add scheduled methods here
}

Schedule Task with Fixed Rate

Let us schedule our first task that executes at a fixed interval of time by using fixedRate property in the @Scheduled annotation:

@Scheduled(fixedRate = 2500)
public void scheduleTaskWithFixedRate() {
    logger.info("Fixed Rate Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

The above task is scheduled to execute every 3 seconds and outputs the following on the console:

Fixed Rate Task: Current Time - 13:36:53
Fixed Rate Task: Current Time - 13:36:56
Fixed Rate Task: Current Time - 13:36:58
Fixed Rate Task: Current Time - 13:37:01
...

The fixedRate task is invoked for every specified interval. The specified interval between method invocations is measured from the start time of each invocation. In other words, the task is executed again even if the previous invocation of the task is not completed.

This option is best suited when each execution of the task is independent. The fixedRate property executes the task at every n milliseconds. It does not wait for any previous execution to finish.

Schedule Task with Fixed Delay

Very similar to fixedRate, the fixedDelay task is invoked for every specified interval but the time is measured from the completion time of each preceding invocation.

In short, the time between the end of last execution and the start of next execution is constant. The task always waits until the previous one is completed.

Consider the following example:

@Scheduled(fixedDelay = 2000)
public void scheduleTaskWithFixedDelay() throws InterruptedException {
    logger.info("Fixed Delay Task: Start Time - {}", formatter.format(LocalDateTime.now()));

    // add some virtual processing time
    TimeUnit.SECONDS.sleep(3);

    logger.info("Fixed Delay Task: End Time - {}", formatter.format(LocalDateTime.now()));
}

The task is scheduled to execute every 2 seconds and prints the start and finish times of the execution. Since we have added 3 seconds of virtual processing time, it will take at least 3 seconds to complete. There will be a delay of 5 seconds between successive invocations:

Fixed Delay Task: Start Time - 14:02:28
Fixed Delay Task: End Time - 14:02:31
Fixed Delay Task: Start Time - 14:02:33
Fixed Delay Task: End Time - 14:02:36
Fixed Delay Task: Start Time - 14:02:38
Fixed Delay Task: End Time - 14:02:41
Fixed Delay Task: Start Time - 14:02:43
Fixed Delay Task: End Time - 14:02:46
...

This option is best suited when the previous execution of the task must be completed before executing again. The fixedDelay property makes sure that is always a delay of n milliseconds between consecutive invocations of a task. For dependent tasks, it is pretty helpful.

Schedule Task with Initial Delay

You can also specify the initial time to wait (in milliseconds) before the first execution of the task begins by using the initialDelay property. It works with both fixedRate and fixedDelay properties.

In the following example, the task is executed first time after a wait of 5 seconds and then it executes normally after every 2 seconds:

@Scheduled(fixedRate = 2000, initialDelay = 5000)
public void scheduleTaskWithFixedRateAndInitialDelay() {
    logger.info("Fixed Rate Task with Initial Delay: Current Time - {}", formatter.format(LocalDateTime.now()));
}

The initialDelay property delays the first execution of the task for the specified milliseconds. After the first execution, the task starts executing normally:

# Server started at 14:42:20
Fixed Rate Task with Initial Delay: Current Time - 14:42:25
Fixed Rate Task with Initial Delay: Current Time - 14:42:27
Fixed Rate Task with Initial Delay: Current Time - 14:42:29
Fixed Rate Task with Initial Delay: Current Time - 14:42:31
...

The initialDelay property helps delay the first execution of the task until the data required to execute the tasks is provided by some other services.

Schedule Task using Cron Expression

Sometimes the fixed-rate and fixed-delay are not enough to fulfill our needs. We want more flexibility to control the schedule of our tasks. For example, we might want to send a newsletter every Thursday or back up our database every week. This is something that cannot be done with the above properties.

That is where the cron expressions come handy. They provide complete flexibility to schedule the tasks, whatever the way you want to choose.

Here is an example task that uses cron expression to execute every minute:

@Scheduled(cron = "0 * * * * ?")
public void scheduleTaskWithCronExpression() {
    logger.info("Cron Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}
Cron Task: Current Time - 15:17:00
Cron Task: Current Time - 15:18:00
Cron Task: Current Time - 15:19:00
Cron Task: Current Time - 15:20:00
...

Let us have few more examples of cron expressions. The following tasks is scheduled to be executed at 4:45 AM on the 10th day of every month:

@Scheduled(cron = "0 45 4 10 * ?")
public void scheduleTaskWithCronExpression2() {
    logger.info("Cron Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

Cron expressions are somehow complex to write and understand. crontab.guru is a nice little tool that makes it easy to generate cron schedule expressions:

# At 12:00 on Sunday
0 0 12 * * Sun

# At 14:15 in every 2nd month
0 15 14 * */2 *

# At 08:00 on every day-of-week from Monday through Friday
0 0 8 * * Mon-Fri

# At 12:30 on every 15th day-of-month if it's on Wednesday
0 30 12 */15 * Wed

Parameterizing the Schedule

In the above examples, we have hard-coded the time intervals and cron expressions. Now if you want to change the execution time of any task, we have to recompile and redeploy the entire application. This is certainly not flexible.

Fortunately, we can make use of Spring Expression Language (SpPL) and store the configuration of the tasks in a properties file:

@Scheduled(fixedRateString = "${fixed-rate.in.milliseconds}")
public void scheduleDynamicTaskWithFixedRate() {
    logger.info("Fixed Rate Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

@Scheduled(fixedDelayString = "${fixed-delay.in.milliseconds}")
public void scheduleDynamicTaskWithFixedDelay() {
    logger.info("Fixed Delay Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

@Scheduled(cron = "${cron.expression}")
public void scheduleDynamicTaskWithCronExpression() {
    logger.info("Cron Dynamic Task: Current Time - {}", formatter.format(LocalDateTime.now()));
}

And our application.properties file will look like this:

application.properties

fixed-rate.in.milliseconds=5000
fixed-delay.in.milliseconds=4000
cron.expression=0 15 5 * * FRI

Custom Thread Pool Configuration

By default, @EnableScheduling annotation creates a thread pool with only one thread. The invocation of all @Scheduled tasks is queued and executed by an only thread. So if you have multiple scheduled tasks in your application, you might see weird behavior of invocation (since the tasks are queued).

But the good thing is you can create your own custom thread pool with multiple threads and configure the application to use that for executing all scheduled tasks:

SchedulerConfig.java

package com.attacomsian.scheduling;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

public class SchedulerConfig implements SchedulingConfigurer {

    @Value("${thread.pool.size}")
    private int POOL_SIZE;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();

        scheduler.setPoolSize(POOL_SIZE);
        scheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        scheduler.initialize();

        taskRegistrar.setTaskScheduler(scheduler);
    }
}

Add the following property to your application.properties file:

thread.pool.size=10

Spring will now create a custom thread pool with 10 threads to execute the tasks. You can also find the thread name in log that was invoked to execute the task:

[ My-Scheduler-4] : Fixed Delay Dynamic Task: Current Time - 17:20:03
[ My-Scheduler-8] : Fixed Rate Dynamic Task: Current Time - 17:20:04
[ My-Scheduler-1] : Fixed Delay Dynamic Task: Current Time - 17:20:07
[ My-Scheduler-7] : Fixed Rate Dynamic Task: Current Time - 17:20:09
[ My-Scheduler-2] : Fixed Delay Dynamic Task: Current Time - 17:20:11
...

Source code: Download the complete source code from GitHub available under MIT license.

Conclusion

That's all folks for task scheduling in Spring Boot. We have learned how to schedule tasks in Spring Boot using @Scheduled annotation. We also learned to create and use a custom thread pool for executing these tasks.

Task scheduling is much valuable to automate complex tasks that would have taken a lot of time doing manually. It saves time that can be spent on other productive things instead of worrying about routine tasks.

✌️ Like this article? Follow @attacomsian on Twitter. You can also follow me on LinkedIn and DEV. Buy me a coffee (cost $3)

Need help to start a new Spring Boot or MEAN stack project? I am available for contract work. Hire me to accomplish your business goals with engineering and design. Let’s talk about your project: hi@attacomsian.com.