Background Job Scheduling in Java
Learn how to schedule fire-and-forget, delayed, and recurring background jobs in a plain Java application using JobRunr's Fluent API.
On this page
This guide walks you through adding background job scheduling to a Java 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
NoteThis guide uses modern Java features such as compact source files (Java 25). JobRunr itself is compatible with Java 8 and higher. On older versions, the examples will need to be adapted.
TipThe complete example project for this guide is available at github.com/jobrunr/jobrunr-examples/java-app.
TipUsing a framework? The Spring Boot, Quarkus, and Micronaut guides use dedicated integrations that make setup even simpler.
Step 1: Add the JobRunr dependency
<dependency>
<groupId>org.jobrunr</groupId>
<artifactId>jobrunr</artifactId>
<version>8.6.1</version>
</dependency>
<!-- Pick one JSON library: Jackson (shown here), Gson, JSON-B -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.1.2<!-- check for a more recent version --></version>
</dependency>
implementation 'org.jobrunr:jobrunr:8.6.1'
// Pick one JSON library: Jackson (shown here), Gson, JSON-B, or Kotlin Serialization
implementation 'tools.jackson.core:jackson-databind:3.1.2' // check for a later version
JobRunr needs a JSON library to serialize job metadata. This example uses Jackson 3, but you can swap it for Jackson 2, Gson, or JSON-B. See Serialization for details.
ProUsing JobRunr Pro? Replace the
jobrunrartifact withjobrunr-proand add the private repository to your build. See Pro Installation for the full setup.
Step 2: Create the EmailService
Create EmailService.java. In a real application these methods would call out to a service like SendGrid or Mailchimp. Here we print to the console so the example runs with no external dependencies.
public class EmailService {
public void sendConfirmation(String email) {
IO.println("Confirmation email sent to " + email);
}
public void sendWelcome(String email) {
IO.println("Welcome email sent to " + email);
}
public void sendWeeklyDigest() {
IO.println("Weekly digest sent to all subscribers");
}
}
NoteJobRunr only requires a Java 8 lambda, e.g.,
() -> emailService.sendConfirmation("test@example.com").EmailServiceis a plain Java class with regular methods; there is noJobinterface to implement or special contract to satisfy.
Step 3: Build the subscription server
Create App.java. Thanks to compact source files (Java 25), no class declaration is needed.
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.io.IOException;
import java.net.InetSocketAddress;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
void main() throws Exception {
var emailService = new EmailService();
JobRunr.configure() // Using JobRunr Pro? Replace `JobRunr` by `JobRunrPro`
.useStorageProvider(new InMemoryStorageProvider())
.useBackgroundJobServer()
.useDashboard()
.initialize();
BackgroundJob.scheduleRecurrently("weekly-digest", Cron.weekly(DayOfWeek.of(1)),
() -> emailService.sendWeeklyDigest());
var server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/subscribe", exchange -> {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { respond(exchange, 405, "Method Not Allowed"); return; }
var email = getEmailFromQueryParams(exchange);
if (email == null) { respond(exchange, 400, "Missing email parameter"); return; }
var jobId = BackgroundJob.enqueue(() -> emailService.sendConfirmation(email));
respond(exchange, 202, "Confirmation email queued with job id " + jobId);
});
server.createContext("/confirm", exchange -> {
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) { respond(exchange, 405, "Method Not Allowed"); return; }
var email = getEmailFromQueryParams(exchange);
if (email == null) { respond(exchange, 400, "Missing email parameter"); return; }
var jobId = BackgroundJob.schedule(Instant.now().plus(3, ChronoUnit.DAYS),
() -> emailService.sendWelcome(email));
respond(exchange, 202, "Welcome email scheduled with job id " + jobId);
});
server.start();
IO.println("Listening on http://localhost:8080");
}
String getEmailFromQueryParams(HttpExchange ex) {
var query = ex.getRequestURI().getQuery();
if (query == null) return null;
for (var param : query.split("&")) {
if (param.startsWith("email=")) return param.substring(6);
}
return null;
}
void respond(HttpExchange ex, int status, String body) throws IOException {
var bytes = body.getBytes();
ex.sendResponseHeaders(status, bytes.length);
ex.getResponseBody().write(bytes);
ex.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/java-app
./gradlew run
Or if you are building from scratch, run your application:
mvn compile exec:java -Dexec.mainClass=App
./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.
- Adopt a framework: Moving to Spring Boot, Quarkus, or Micronaut? The dedicated integrations auto-configure JobRunr from your application properties file.
- JobRunr ProScale with JobRunr Pro: Unlock batches, job chaining, priority queues, rate limiting, and an advanced dashboard. See JobRunr Pro.
