Code Review Videos > Kotlin > Kotlin Task Scheduling: Timer vs. Quartz vs. Cron4j vs. Spring Scheduler

Kotlin Task Scheduling: Timer vs. Quartz vs. Cron4j vs. Spring Scheduler

Out of nothing other than sheer curiosity, I was wondering how Kotlin (and by extension, Java) handles scheduling tasks. Let’s say that we have a function that we wish to run every 5 seconds. How can we go about achieving that in Kotlin?

That was the question I set out to answer. In this post I will share with you my (admittedly basic) observations and learnings.

Here is a brief introduction to the four libraries we’ll be covering in this beginner friendly tour of task scheduling with Kotlin:

  1. Timer: Timer is a comparatively simple scheduling library included in the Kotlin standard library. It allows you to schedule tasks to run at a fixed rate or delay. It seems like an obvious choice for simple scheduling needs and appears nice and easy to use.
  2. Quartz: Quartz is a much more powerful scheduling library that provides features like multi-threaded execution, recovery from missed tasks, and support for complex scheduling requirements using cron expressions. It appears to be the best choice for more complex scheduling needs.
  3. Cron4j: Cron4j is another powerful scheduling library that provides similar features to Quartz, but with a simpler API and appears a little more unloved. It too supports cron expressions for complex scheduling requirements.
  4. Spring Scheduler: Spring Scheduler is a scheduling library that is part of the Spring Framework. It provides a simple and flexible way to schedule tasks using annotations. It is probably the best choice if you are already using Spring in your project.

What Task Will We Be Scheduling?

To give each library a fair chance, we will try to do the same thing using all four task scheduling choices.

The aim is to be able to run a simple println function every 5 seconds that prints out the current time.

I know this is a very basic test. This is more about getting some hands on with each library, and actually using Kotlin, rather than trying to do too much, too soon.

Hopefully the code to actually achieve the visual outcome will be as simple as:

println("This task ran at: ${Date.from(Instant.now())}")Code language: Kotlin (kotlin)

With the scene set, let’s get on with the show.

Kotlin’s StdLib Timer

By far the simplest way to schedule a task using Kotlin is to use the Timer class.

I believe Timer is a standard Java class, and the scheduler functionality is added using Kotlin extension functions. That’s certainly how the documented code seems to explain it.

The code is really straightforward:

Timer()
    .schedule(timerTask {
        println("Hello from StdLib Timer: ${Date.from(Instant.now())}")
    }, 0L, 5000L)Code language: Kotlin (kotlin)

No variables needed here, but we could modify this code to store an instance of the Timer class and then do further things with it. An example of that follows below.

Timer is the class from the Kotlin Standard Library (Std Lib). We call Timer() to create a new instance of the class, and then immediately use that instance by calling the schedule method.

schedule has a variety of ways in which it can be called, but all start by taking an instance of TimerTask as the first argument.

Probably the most confusing part of this code, for me at least, was exactly how the call of timerTask { ... } ends up creating a new instance of a TimerTask. That syntax was new to me. What’s actually happening there deserves its own post, so we won’t dive into it here.

If we continue on, timerTask creates a new instance of a TimerTask and the code inside the lambda (the { and }) is what will actually be executed on each tick of the timer. In this case it’s the standard println output we covered above.

The two other arguments: 0L and 5000L are two time values expressed as Longs. I’m taking a guess here that the reason to use Longs rather than Ints is down to the use of milliseconds. Ints have a much smaller maximum range than longs, some like 2 billion for Ints versus 9 Quintilian for Longs.

java int vs long 3 commas club

To put that into perspective, a billion has three commas, a Quintilian has six.

0L is the initial delay – in this case, run immediately.

5000L is the interval.

Both are expressed in milliseconds.

So immediately when the code is executed, and every 5 seconds thereafter, the println function should be called.

Hello from StdLib Timer: Mon May 08 11:42:48 BST 2023
Hello from StdLib Timer: Mon May 08 11:42:53 BST 2023
Hello from StdLib Timer: Mon May 08 11:42:58 BST 2023Code language: Shell Session (shell)

Using A Timer Variable

I said above that you could expand the code further to store the Timer instance into a variable, and then ‘do stuff’ with it.

The basic example I came up with was as follows:

    val timer = Timer()

    timer.schedule(timerTask {
        println("Hello from StdLib Timer: ${Date.from(Instant.now())}")
    }, 0L, 5000L)

    Thread.sleep(25000L)
    timer.cancel()Code language: Kotlin (kotlin)

The println remains the same, but by throwing in a Thread.sleep call, we can then wait around for 25 seconds and terminate the execution cleanly:

Hello from StdLib Timer: Mon May 08 11:44:09 BST 2023
Hello from StdLib Timer: Mon May 08 11:44:14 BST 2023
Hello from StdLib Timer: Mon May 08 11:44:19 BST 2023
Hello from StdLib Timer: Mon May 08 11:44:24 BST 2023
Hello from StdLib Timer: Mon May 08 11:44:29 BST 2023

Process finished with exit code 0Code language: CSS (css)

There’s likely more you can do here, but that was enough for me to prove the concept.

Timer Pros

Timer seems like it is a good choice for fairly simple tasks that need to be executed periodically with a fixed delay between calls.

Perhaps if you need to periodically download a file from a remote server and save it locally, Timer might be sufficient.

Timer Cons

From what I’ve read, Timer starts to lack when things get a little more serious.

Timer runs in a single thread, so if a task takes a long time to execute, it can delay the execution of subsequent tasks. This can lead to scheduling irregularities.

Timer cannot recover from missed tasks. If a task is scheduled to run every 5 seconds, but the previous execution took 10 seconds, the next execution will be delayed by 5 seconds. This can cause the execution times to drift over time, which may not be acceptable in some applications.

Timer does not provide a way to express complex scheduling requirements, such as “run this task every weekday at 8:00 AM”.

Kotlin Task Scheduling With Quartz

Quartz was the second task scheduling library I tried.

That’s why it’s going here as the second library.

However, I genuinely think it is the best library out of all four that I tried.

The code isn’t that much more complex than using the Timer version above, but the flexibility is substantially greater, even for a newbie like myself.

Setup

To use Quartz you will need to add the dependency to your project.

This is as simple as updating the build.gradle.kts file as follows:

dependencies {
    implementation("org.quartz-scheduler:quartz:2.3.2")

    testImplementation(kotlin("test"))
}Code language: JavaScript (javascript)

I’m using version 2.3.2 because that’s the latest stable version. How I found that out was by using Maven Repository:

quartz task scheduler on maven repository

If you then click on the specific version number – 2.3.2 in this case – you get taken to a page where you can then select your build system. I am using Gradle with Kotlin, so I got the following:

maven repository select dependency gradle kotlin

Which is pretty nice. Just copy / paste that into your build.gradle.kts file and IntelliJ should pop up a little icon to let your synchronise your new project dependencies:

load gradle changes intellij

That should be all you need to do.

Implementation

Quartz is a Java library, and so working with it from Kotlin wasn’t quite as easy as following the good number of tutorials over on the Quartz GitHub repo. Sadly.

Mostly I used tutorial-lesson-01.md as my point of reference. I’ve not linked directly to that as it’s a GitHub blob address and they have a bad tendency to break.

However, it wasn’t that hard for me to figure out, either. And remember, I am both a Kotlin AND Java newbie. So if I can do it, you bet you can too.

Here’s the code I came up with:

package org.example.schedulers

import org.example.jobs.QuartzJob
import org.quartz.JobBuilder
import org.quartz.SimpleScheduleBuilder
import org.quartz.TriggerBuilder
import org.quartz.impl.StdSchedulerFactory

fun quartzScheduler() {
    val schedulerFactory = StdSchedulerFactory()
    val scheduler = schedulerFactory.scheduler
    scheduler.start()

    val job = JobBuilder.newJob(QuartzJob::class.java).build()

    val trigger = TriggerBuilder.newTrigger()
        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
        .build()

    scheduler.scheduleJob(job, trigger)
}
Code language: Kotlin (kotlin)

That’s quite a lot of code, and if you gave it more than a cursory glance (hey, I skim read tech tutorials too, don’t feel bad!) then you may have been like, Chris, where the heck is the println ?

Good point.

I split it out into another class.

package org.example.jobs

import org.quartz.Job
import org.quartz.JobExecutionContext
import java.time.Instant
import java.util.Date

class QuartzJob : Job {
    override fun execute(context: JobExecutionContext?) {
        println("Hello from Quartz Job: ${Date.from(Instant.now())}")
    }
}Code language: Kotlin (kotlin)

This could be inlined. I will cover how to do that below. The reason I didn’t inline it is that in a real project, there is almost no way I would ever have the complexity necessary to use Quartz, whilst simultaneously having the simplicity to inline the called code. I hope that makes sense.

Code Walkthrough

Let’s cover the code now, line by line.

val schedulerFactory = StdSchedulerFactory()

Fairly straightforward. We create a new instance of the StdSchedulerFactory class. I copied this straight from the tutorial docs, but imported the dependency path (line 7).

Next comes a little Kotlin shorthand magic:

kotlin use of getter method instead of property access syntax

The code I started with was:

val scheduler = schedulerFactory.getScheduler()

Looks fairly standard, right?

I need a new instance of the Scheduler which the schedulerFactory can provide. So I call that method. But IntelliJ pops up a little hint / suggested refactoring to drop the getter method, and instead replace it with property access syntax.

I’d actually argue that this is very unintuitive for non-Kotlin natives. However, I guess if you are fluent with Kotlin, it is standard stuff.

Whilst not obvious at all from the code sample above – because the Syntax Highlighting plugin just isn’t that good – things do look slightly more interesting in IntelliJ:

So yeah, a nice shade of pink, or purple. I’m not sure which. But it looks somehow different.

OK, onwards.

scheduler.start() – starts the scheduler instance. Again, straight from the tutorial docs – Once a scheduler is instantiated, it can be started, placed in stand-by mode, and shutdown.

That’s all the mandatory setup stuff done.

Calling Through To Java

On line 14 we have val job = JobBuilder.newJob(QuartzJob::class.java).build()

Classes creating classes. Coming from a more functional language like JavaScript, this might feel a little heavy. But it’s fine. It’s just different.

newJob takes a class that implements Quartz’s Job interface. In this case, that would be the separate class I created above, the QuartzJob class.

That interface defines a single method: execute, which is the code that will be called every time to job is executed.

The most interesting part of this syntax, as far as I am concerned, is QuartzJob::class.java.

In Kotlin, when we suffix a class name with .java suffix we can get a reference to the actual Java class object that corresponds to the Kotlin class.

As best I understand it. the Kotlin class keyword returns a Kotlin KClass object, which provides information about that class at runtime.

However, sometimes such as in this example, we need to access the Java Class object for interoperability with Java code or certain Java-based frameworks or libraries, like the Quartz library in this case.

The .java suffix is a way to get the Java Class object from the KClass object in Kotlin. It is seemingly equivalent to calling the Java getClass() method on an object.

Building & Starting The Scheduled Job

Lines 16 through 20 cover off creating a trigger. A Trigger is the way in which Jobs are scheduled by Quartz.

First we create a new instance of a Trigger with TriggerBuilder.newTrigger().

Then with that Trigger we define the schedule.

Even if you don’t understand any other part of this code, SimpleScheduleBuilder.repeatSecondlyForever(5) is surely the most intuitive command we have covered.

The last method in the fluent chain of line’s 16 through 18 calls .build(). This is the Builder creational design pattern in action, which allows the API consumer (us, in this case) to create what are likely potentially complex objects in a step-by-step manner.

Forgetting to call the .build() method results in a bad time, because you end up with an instance of the Builder, rather than the object you intended to build. Whoops.

Finally, after all that, we can start the job:

scheduler.scheduleJob(job, trigger)Code language: Kotlin (kotlin)

As the code suggests, this schedules the job to be executed by the scheduler at the specified trigger interval. In this case, the job will be executed every 5 seconds.

The Outcome

The outcome is that this code then ticks along, every 5 seconds printing out the date and time to the console, until the program is terminated:

Hello from Quartz Job: Mon May 08 19:25:31 BST 2023
Hello from Quartz Job: Mon May 08 19:25:36 BST 2023
Hello from Quartz Job: Mon May 08 19:25:41 BST 2023
Hello from Quartz Job: Mon May 08 19:25:46 BST 2023

Process finished with exit code 130 (interrupted by signal 2: SIGINT)Code language: CSS (css)

And whilst that is a lot of words to walk through this code, I feel it’s actually fairly easy to work with, when not having to give a fairly thorough explanation of each line’s purpose.

The main thing is that this code does exactly what we wanted. It prints out the date / time in a nicely formatted manner, in a clean and extensible fashion, and with a minimum of fuss.

Can the remaining libraries here compete?

Let’s see.

Quartz Pros

Personally I found Quartz to be the nicest API to work with. It felt like I knew what was happening, and was in complete control at all times.

Here are some other plus points I discovered about Quartz:

  • Multi-threaded execution, which ensures that long-running tasks do not block the execution of other tasks.
  • Recovery from missed tasks, which ensures that the task is executed at the next scheduled time even if it was missed due to a previous execution taking too long.
  • Support for complex scheduling requirements, such as cron expressions that allow you to specify complex schedules like “run this task every weekday at 8:00 AM”.

Quartz Cons

The learning curve is higher than Timer.

It’s another dependency to add to the project.

There’s comparatively a lot more code required to do all the setup steps than in the other examples.

With so many features and capabilities, maybe it’s going to be less performant or memory hungry than the other options? Not sure on this one.

Kotlin Task Scheduling with Cron4j

Cron4J was the third task scheduling library I tried.

I was a little concerned when I took a look at Maven Repository and saw this library hadn’t seen an update since July 2013.

cron4j maven repository

However, with no known vulnerabilities, so long as the code is feature complete, it needn’t have to be regularly updated to do what it set out to achieve.

The code for the Cron4J implementation is short and sweet:

package org.example.schedulers

import it.sauronsoftware.cron4j.Scheduler
import org.example.jobs.Cron4JJob

fun cron4JScheduler() {
    val scheduler = Scheduler()
    scheduler.schedule("* * * * *", Cron4JJob())
    scheduler.start()
}Code language: Kotlin (kotlin)

And the code for my Cron4JJob class is as follows:

package org.example.jobs

import java.time.Instant
import java.util.Date

class Cron4JJob : Runnable {
    override fun run() {
        println("Hello from Cron4J Job: ${Date.from(Instant.now())}")
    }
}

So, super easy really.

However, there’s one big problem here.

It doesn’t actually solve the requirement of running every 5 seconds.

In the cron4JScheduler function, we first create a new instance of the Scheduler class provided by the cron4j library and we assign it to a variable called scheduler.

Then we have the following line:

scheduler.schedule("* * * * *", Cron4JJob())Code language: Kotlin (kotlin)

And this where the problem lies.

We have to use the cron expression syntax, and as best I am aware that doesn’t have the granularity of seconds.

The "* * * * *" syntax means “run every minute”.

You can use a website like Crontab.guru to better understand / create cron expressions:

crontab guru output

There’s no way to change this to run more frequently than once a minute. With a crontab you can come up with hacks / workarounds to run more frequently, either using shell scripts or multiple calls with sleep commands, probably other methods, too. But not so here.

I mean, this works well enough. It just doesn’t do what we actually want it to do.

Hello from Cron4J Job: Mon May 08 19:35:00 BST 2023
Hello from Cron4J Job: Mon May 08 19:36:00 BST 2023
Hello from Cron4J Job: Mon May 08 19:37:00 BST 2023
Hello from Cron4J Job: Mon May 08 19:38:00 BST 2023

Process finished with exit code 130 (interrupted by signal 2: SIGINT)Code language: CSS (css)

And that then only leaves us with one final option to test.

Cron4J Pros

It’s great if you are looking for a library that puts cron expressions front and centre.

Compared to Quartz there was less setup involved from a code writing perspective.

Fewer options for configuration mean fewer things to learn or get confused by.

Cron4J Cons

It’s not under active development.

The project has been archived on GitHub so there’s no way to see previous issues or pull requests. This alone is enough would be enough to make me avoid it.

Kotlin Task Scheduling with Spring Scheduler

The code to print a message every five seconds using Spring Boot is the easiest* of the lot.

And by easiest, I mean there’s clearly a heck of a lot happening behind the scenes to make this work, which we not only do not see, but don’t even need to understand.

That may be perfect for you.

Or it may leave you with an uneasy feeling.

I’m somewhere in the middle. I trust Spring Boot to be very well made, well tested, and well meaning.

But I’d love to know exactly how it works under the hood. However, that is a huge ask. Spring is massive. It’s the sort of thing you could devote a year too and barely scratch the surface.

Anyway, in order to get this next example working, I had to switch to a brand new project. I could have made a Spring Boot project and then imported the other two dependencies from above, but somehow that didn’t feel right.

Instead, I chose to split the Spring Boot / Spring Scheduler example into a standalone project.

There’s probably a way to import and run just the Spring Scheduler code. But I’m not that skilled at this time, so this is the way I could get it to work.

Implementation

As above, I created a brand new project using the Spring Initializr.

spring initializr for spring scheduler

There’s nothing special about this.

No extra dependencies needed.

The main thing, I think, is the build.gradle.kts file, which looks like this:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id("org.springframework.boot") version "3.0.6"
	id("io.spring.dependency-management") version "1.1.0"
	kotlin("jvm") version "1.7.22"
	kotlin("plugin.spring") version "1.7.22"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs = listOf("-Xjsr305=strict")
		jvmTarget = "17"
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}Code language: Kotlin (kotlin)

I did not change this in any way from the generated version from Spring Initializr.

Code Walkthrough

Here’s the full code to make this run:

package com.example.testingschedules

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.Instant
import java.util.*

@SpringBootApplication
@EnableScheduling
class TestingSchedulesApplication

fun main(args: Array<String>) {
	runApplication<TestingSchedulesApplication>(*args)
}

@Component
class MyTask {
	@Scheduled(fixedRate = 5000)
	fun printMessage() {
		println("Hello from Spring Boot Job: ${Date.from(Instant.now())}")
	}
}
Code language: Kotlin (kotlin)

I’ve highlighted everything I needed to add to the starter code (which is everything else that is not highlighted) to make this work.

I’ll cover off the non-standard stuff.

Line’s 8 and 9 are required for printing out the date / time on line 23.

@EnableScheduling is the annotation that is used to enable Spring’s scheduling functionality. I found this on the docs, and whilst this is an annotation, the documentation is extensive – albeit found via code docs.

Lines 19-25 again come from the Spring Scheduler tutorial.

We need the @Component annotation to mark that class being Spring-managed. Spring will then handle creation of an instance of MyTask for us without us having to explicitly create a new instance.

We add yet another annotation on line 21 – @Scheduled(fixedRate = 5000).

I think fixedRate is a slightly less friendly name than we saw in Quartz which had repeatSecondlyForever, but the outcome is the same. This code will be executed every 5 seconds, forever.

And finally we have the actual function code, which is basically the same as we’ve had in the previous three examples.

So, much easier in many regards, but clearly a lot happening behind the scenes that we are completely unaware of.

Spring Scheduler Pros

Backed by the Spring Framework, this is going to be a popular, widely used, battle tested approach to solving the problem of scheduled tasks in Kotlin or Java.

Even though I had to import a full Spring Boot project, the code to get a scheduled task running – from my point of view – was very simple. I appreciate there’s a lot going on under the hood to provide me with that simplicity.

It’s already part of the Spring Framework. It wasn’t like I had to import the Spring Framework and another Spring dependency. So if you’re using Spring Boot, you already have this.

Spring Scheduler Cons

I had to set up a whole new Spring Boot project just to try this out. It may very well be that, as a newbie, I don’t know how to pull in just the Spring Scheduler. But the docs did not spell this out. Maybe it’s not even possible?

That brings about the second point: the learning curve for Spring Framework is scary intimidating. I’d love to know more about it, and am actively learning, but boy is there a lot to learn. I feel like I brought a steamroller to push in a thumbtack.

Wrapping Up

I’m glad I’ve had a chance to try out Timer, Quartz, Cron4J, and Spring Scheduler. Each approach has given me a better understanding of how to schedule simple and small tasks in a Kotlin project.

Timer is the obvious choice to reach for first. It’s built in, simple to use, and does the job.

However, when things get more serious, I’d be looking at either Quartz or Spring Scheduler.

If I was already using Spring Framework then Spring Scheduler is a no-brainer.

I would use Quartz in the first instance for everything else.

For me, Cron4J is not a library I would reach for. In archive mode on GitHub, and with pull requests and GitHub issues hidden, I have no idea what potential headaches may arise, and no direct hope of talking to the devs if things did go wrong. Aside from that, the library itself seems fine.

Ultimately it’s not hard to try out each library / approach. As ever with tasks like this, it’s probably more scary / off-putting just getting going. Once you’re underway, it’s plain sailing. And hey, you don’t have to type up all your findings like I do 🙂

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.