Background Job Scheduling in Kotlin
Learn how to schedule fire-and-forget, delayed, and recurring background jobs in a Kotlin application using JobRunr with Kotlin lambdas and Kotlin Serialization.
On this page
This guide walks you through adding background job scheduling to a Kotlin application. As a practical example, we’ll build a small HTTP server for a newsletter subscription service, using JobRunr to handle three common background job patterns:
- Fire-and-forget job: send a confirmation email without making the subscriber wait
- Delayed job: follow up a few days after confirmation with a welcome email
- Recurring job: send the weekly digest on a fixed schedule
TipThe complete example project for this guide is available at github.com/jobrunr/jobrunr-examples/kotlin-app.
Step 1: Add the dependencies
// build.gradle.kts
plugins {
kotlin("jvm") version "2.2.0"
kotlin("plugin.serialization") version "2.2.0"
}
dependencies {
implementation("org.jobrunr:jobrunr:8.6.1")
// Optional but recommended for Kotlin projects: Kotlin-native features
implementation("org.jobrunr:jobrunr-kotlin-support:8.6.1")
// Required when using Kotlin Serialization as JSON mapper
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") // check for a more recent version
}
The package jobrunr-kotlin-support is not required to schedule jobs: the core jobrunr library already understands Kotlin lambdas. The package becomes useful when you want to use Kotlin Serialization instead of Jackson or Gson, resolve job dependencies from a Koin container via KoinJobActivator, or leverage Exposed transactions (JobRunr Pro). When both jobrunr-kotlin-support and kotlinx-serialization-json are on the classpath, JobRunr picks up Kotlin Serialization automatically as its JSON mapper, no extra wiring needed.
TipUsing a framework with Kotlin? The Spring Boot, Quarkus, and Micronaut guides use dedicated integrations that make setup even simpler. You can add
jobrunr-kotlin-supportalongside any framework starter to unlock Kotlin-native features.
ProUsing JobRunr Pro? Replace
jobrunrwithjobrunr-proandjobrunr-kotlin-supportwithjobrunr-pro-kotlin-support, then add the private repository. See Pro Installation for the full setup.
Step 2: Create the EmailService
Create EmailService.kt. In a real application these methods would call a service like SendGrid or Mailchimp. Here we print to the console so the example runs with no external dependencies.
class EmailService {
fun sendConfirmation(email: String) {
println("Confirmation email sent to $email")
}
fun sendWelcome(email: String) {
println("Welcome email sent to $email")
}
fun sendWeeklyDigest() {
println("Weekly digest sent to all subscribers")
}
}
NoteJobRunr only requires a Kotlin lambda, e.g.,
{ emailService.sendConfirmation("test@example.com") }.EmailServiceis a plain Kotlin class with regular methods; there is noJobinterface to implement or special contract to satisfy.
Step 3: Build the subscription server
Create App.kt.
Three things happen here:
- a subscriber hits
POST /subscribeand a confirmation email is sent as soon as possible (fire-and-forget job) - once they confirm via
POST /confirm, a welcome email is scheduled for three days later (delayed job) - a weekly digest goes out to all subscribers every Monday (recurring job)
import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import org.jobrunr.configuration.JobRunr
import org.jobrunr.scheduling.BackgroundJob
import org.jobrunr.scheduling.cron.Cron
import org.jobrunr.storage.InMemoryStorageProvider
import java.net.InetSocketAddress
import java.time.DayOfWeek
import java.time.Instant
import java.time.temporal.ChronoUnit
fun main() {
val emailService = EmailService()
JobRunr.configure() // Using JobRunr Pro? Replace `JobRunr` with `JobRunrPro`
.useStorageProvider(InMemoryStorageProvider())
.useBackgroundJobServer()
.useDashboard()
.initialize()
BackgroundJob.scheduleRecurrently("weekly-digest", Cron.weekly(DayOfWeek.MONDAY)) {
emailService.sendWeeklyDigest()
}
val server = HttpServer.create(InetSocketAddress(8080), 0)
server.createContext("/subscribe") { exchange ->
if (exchange.requestMethod != "POST") { exchange.respond(405, "Method Not Allowed"); return@createContext }
val email = exchange.getEmailFromQueryParams()
?: run { exchange.respond(400, "Missing email parameter"); return@createContext }
val jobId = BackgroundJob.enqueue { emailService.sendConfirmation(email) }
exchange.respond(202, "Confirmation email queued with job id $jobId")
}
server.createContext("/confirm") { exchange ->
if (exchange.requestMethod != "POST") { exchange.respond(405, "Method Not Allowed"); return@createContext }
val email = exchange.getEmailFromQueryParams()
?: run { exchange.respond(400, "Missing email parameter"); return@createContext }
val jobId = BackgroundJob.schedule(Instant.now().plus(3, ChronoUnit.DAYS)) {
emailService.sendWelcome(email)
}
exchange.respond(202, "Welcome email scheduled with job id $jobId")
}
server.start()
println("Listening on http://localhost:8080")
}
fun HttpExchange.getEmailFromQueryParams(): String? {
val q = requestURI.query ?: return null
val value = q.substringAfter("email=", missingDelimiterValue = "")
return value.ifEmpty { null }
}
fun HttpExchange.respond(status: Int, body: String) {
val bytes = body.toByteArray()
sendResponseHeaders(status, bytes.size.toLong())
responseBody.write(bytes)
close()
}
There is a lot to unpack from this simple code. Both HTTP handlers return 202 Accepted immediately, so the caller never waits for email delivery. If the mail provider is temporarily down, the job is automatically retried. The weekly digest runs on its own schedule, independently of any HTTP request. Switch to a persistent database and you also get crash resilience: if the server goes down mid-job, JobRunr picks it back up on restart. That is already a quite powerful setup for a few lines of code.
Most of this code sets up the HTTP server. Here is what JobRunr contributes:
- Configure and start JobRunr: sets up the storage, job processing server (aka background job server), and dashboard. The job server and dashboard are opt-in; storage is the only required piece.
- Register a recurring job: JobRunr triggers this every Monday.
- Enqueue a fire-and-forget job: picked up by a worker as soon as one is free.
- Schedule a delayed job: stored and executed automatically 3 days from now.
Tip
useBackgroundJobServeranduseDashboardare opt-in. Both accept configuration options, and each has a conditional variant (useBackgroundJobServerIf,useDashboardIf) that lets you toggle them via an environment variable or application argument.
ImportantJobs stored in
InMemoryStorageProviderdo not survive restarts. When you are ready to go to production, swap it for a real database. See Storage.
Step 4: Try it out
The fastest way to get running is to clone the example project directly:
git clone https://github.com/jobrunr/jobrunr-examples.git
cd jobrunr-examples/kotlin-app
./gradlew run
Or if you are building from scratch, run your application:
./gradlew run
Then hit the endpoints:
# Subscribe: sends a confirmation email immediately
curl -X POST "http://localhost:8080/subscribe?email=test@example.com"
# Confirm: schedules a welcome email for 3 days from now
curl -X POST "http://localhost:8080/confirm?email=test@example.com"
Open the dashboard at http://localhost:8000/dashboard:
- The Dashboard tab shows statistics about your job processing system.
- The Jobs tab shows the various jobs in queue or processed by JobRunr.
- The Recurring Jobs tab shows details about your periodic jobs.
- The Servers tab shows the servers executing your jobs.
Next steps
- Use a real database:
InMemoryStorageProvideris fine for local development. Before going to production, switch to a persistent StorageProvider backed by your existing SQL or NoSQL database. - Configure the job scheduler: The Fluent API reference covers worker counts, custom retry policies, JMX, Micrometer metrics, and more.
- Use Koin for dependency injection:
KoinJobActivatorfromjobrunr-kotlin-supportlets JobRunr resolve job classes from the Koin container, so your job methods run with the full DI context at execution time. - Adopt a framework: Moving to Spring Boot, Quarkus, or Micronaut? The dedicated integrations auto-configure JobRunr from your application properties file, and
jobrunr-kotlin-supportcan be added alongside any of them. - JobRunr Pro Scale with JobRunr Pro: Unlock batches, job chaining, priority queues, rate limiting, Exposed transaction support, and an advanced dashboard. See JobRunr Pro.
