Skip to content

Latest commit

 

History

History
681 lines (497 loc) · 14.7 KB

File metadata and controls

681 lines (497 loc) · 14.7 KB

footer: JobRunR - Easy Java Job Scheduling: Java Vienna 2026-02-16 - Dominik Dorn slidenumbers: true

JobRunR

Easy Java Job Scheduling

Java Vienna - Dominik Dorn

^ Welcome everyone! Today we'll look at JobRunr - a library that makes background job processing in Java surprisingly simple.


About Me

Dominik Dorn


Agenda

  1. What is JobRunr?
  2. Core Concepts & Architecture
  3. Scheduling Jobs
  4. The Dashboard
  5. Storage Engines
  6. Framework Integrations
  7. Live Demos
  8. Best Practices

What is JobRunr?

Background job processing for Java

  • Create background jobs with just Java code
  • No XML, no config files, no message brokers
  • Jobs survive application restarts
  • Built-in dashboard for monitoring
  • Open source (LGPL 3.0)

^ JobRunr lets you offload work from the request thread. Send emails, generate reports, process files - all in the background.


Why Background Jobs?

  • Sending emails & notifications
  • Generating PDF reports
  • Image/video processing
  • Data imports/exports
  • Heavy computations
  • Scheduled maintenance tasks
  • Anything that shouldn't block the user

Architecture Overview

  • JobScheduler - analyzes your lambda, serializes it as JSON, stores it
  • StorageProvider - SQL or NoSQL database (PostgreSQL, H2, SQLite, MongoDB...)
  • BackgroundJobServer - polls for jobs, deserializes and executes them
  • Dashboard - built-in web UI on port 8000

^ What gets serialized? The fully qualified class name, the method name, parameter types, and argument values - all as JSON. That's how JobRunr can reconstruct and invoke the method later on a different thread or even a different server.


Architecture Overview

Your App --> JobScheduler --> StorageProvider (DB)
                                    |
                             BackgroundJobServer
                              (Worker Threads)
                                    |
                               Dashboard :8000

What's stored in the DB?

CREATE TABLE jobrunr_jobs (
  id             CHAR(36)        PRIMARY KEY,
  version        INT             NOT NULL,
  jobasjson      TEXT            NOT NULL,
  jobsignature   VARCHAR(512)    NOT NULL,
  state          VARCHAR(36)     NOT NULL,
  createdat      TIMESTAMP       NOT NULL,
  updatedat      TIMESTAMP       NOT NULL,
  scheduledat    TIMESTAMP,
  recurringjobid VARCHAR(128)
);

^ Tables are created automatically by JobRunr. The key column is jobasjson - a single TEXT column holding the entire job as JSON. State and timestamps are denormalized for fast queries and indexing.


What's stored in the DB?

The jobasjson column

{
  "id": "019c5dee-64c6-7d72-87d6-8fbe3245ec16",
  "jobDetails": {
    "className": "com.example.MyService",
    "methodName": "doWork",
    "jobParameters": [{
      "className": "java.lang.String",
      "object": "Hello World"
    }]
  },
  "jobHistory": [
    {"state": "ENQUEUED",  "createdAt": "2026-02-14T20:53:50Z"},
    {"state": "PROCESSING","serverName": "MacBook-Pro-2.local"},
    {"state": "SUCCEEDED", "processDuration": 15.10}
  ]
}

^ className + methodName + parameters - that's all JobRunr needs to reconstruct and invoke your method on any server. The jobHistory tracks every state transition.


Core Concepts

Job Lifecycle

ENQUEUED - Job is ready to be processed PROCESSING - Job is being executed by a worker SUCCEEDED - Job completed successfully FAILED - Job failed (will retry automatically) SCHEDULED - Job waiting for scheduled time DELETED - Job was deleted

^ Jobs move through these states automatically. Failed jobs retry with exponential backoff - up to 10 times by default.


Getting Started - Fluent API

public static void main(String[] args) {
    JobRunr.configure()
        .useStorageProvider(new InMemoryStorageProvider())
        .useBackgroundJobServer()
        .useDashboard()
        .initialize();

    BackgroundJob.enqueue(
        () -> System.out.println("Simple!")
    );
}

^ This is literally all you need. Five lines of configuration, one line to enqueue a job. Let me show you.


DEMO

example-fluent

Simplest possible setup

^ Show Main.java, run it, open dashboard at localhost:8000


Fire & Forget Jobs

BackgroundJob.enqueue(
    () -> myService.doWork("Hello")
);
  • Executes immediately in background
  • Method call is serialized to JSON
  • Returns immediately (non-blocking)
  • Survives application restarts

Delayed / Scheduled Jobs

BackgroundJob.schedule(
    Instant.now().plus(Duration.ofHours(3)),
    () -> myService.sendReminder(userId)
);
  • Execute at a specific time
  • Accepts Instant, ZonedDateTime, LocalDateTime
  • Polled every 15 seconds (configurable)

Recurring Jobs

// Using CRON expressions
BackgroundJob.scheduleRecurrently(
    "daily-report",
    Cron.daily(),
    () -> reportService.generateDailyReport()
);

// Using Duration
BackgroundJob.scheduleRecurrently(
    "health-check",
    Duration.ofMinutes(5),
    () -> healthService.checkAll()
);

^ CRON helpers: Cron.daily(), Cron.hourly(), Cron.weekly(), etc. Or pass a standard CRON string.


@Recurring Annotation

@Component
public class MyService {

    @Recurring(id = "my-recurring-job",
               cron = "0 0/15 * * *")
    @Job(name = "My recurring job")
    public void doRecurringJob() {
        System.out.println("Doing recurring work");
    }
}
  • Works with Spring, Quarkus, Micronaut
  • Declarative - no scheduling code needed
  • Automatically registered on startup

Bulk Enqueuing

Stream<User> users = userRepository.findAll();

BackgroundJob.enqueue(
    users,
    (user) -> mailService.sendNewsletter(user.getId())
);
  • Efficient batch saving to database
  • Works with any Stream<T>
  • Much faster than individual enqueue calls

Job Arguments & Serialization

What can be passed as arguments?

  • Primitive types and wrappers
  • Strings
  • java.time.* types
  • Custom objects (must serialize to JSON)

Job Arguments - Best Practice

// GOOD - pass the ID
BackgroundJob.enqueue(
    () -> service.processOrder(order.getId()));

// AVOID - pass the whole entity
BackgroundJob.enqueue(
    () -> service.processOrder(order));
  • Arguments are serialized to JSON and stored in DB
  • Data may be stale by the time the job runs
  • Pass IDs, load fresh data in the job method

^ The entity might change between enqueue and processing time. Always pass IDs and load the entity inside the job.


JobRequest Pattern

public record MyJobRequest(String input)
        implements JobRequest {

    public Class<MyJobRequestHandler> getJobRequestHandler() {
        return MyJobRequestHandler.class;
    }
}

@ApplicationScoped
public class MyJobRequestHandler
        implements JobRequestHandler<MyJobRequest> {

    public void run(MyJobRequest request) {
        System.out.println("Processing: " + request.input());
    }
}

^ Alternative to lambdas. Required for GraalVM native images. Follows the command/handler pattern.


@Job Annotation

@Job(name = "Sending email to %1",
     retries = 5)
public void sendEmail(String recipient,
                      String subject) {
    // ...
}
  • Custom display name in dashboard (%0, %1 for args)
  • Configure retries per job
  • Add labels for filtering

JobContext - Progress & Logging

public void doWork(String arg, JobContext ctx) {
    JobDashboardProgressBar progress =
        ctx.progressBar(100);

    for (int i = 0; i < 100; i++) {
        // do work
        progress.setProgress(i + 1);
    }
}

// Enqueue with JobContext.Null placeholder
BackgroundJob.enqueue(
    () -> service.doWork("test", JobContext.Null));

^ The progress bar and logger output appear live in the dashboard. Great for long-running jobs.


The Dashboard

Built-in web UI at http://localhost:8000

  • Real-time job monitoring
  • Job states overview
  • Detailed job inspection
  • Recurring job management
  • Server monitoring
  • No extra setup required

Dashboard - Job Overview

  • See all jobs grouped by state
  • Enqueued, Scheduled, Processing, Succeeded, Failed
  • Click any job for details
  • Re-queue failed jobs manually

^ Let me show this in a live demo.


Dashboard - Job Details

  • Full state transition history
  • Method signature and arguments
  • Exception messages and stack traces
  • Execution duration
  • Retry attempt count and timings
  • Progress bar (if using JobContext)

Dashboard - Recurring Jobs

  • View all registered recurring jobs
  • Next scheduled execution time
  • Manually trigger a recurring job
  • Delete a recurring job
  • CRON expression display

Dashboard - Servers

  • Monitor all BackgroundJobServer instances
  • Worker count per server
  • Server state (running/paused)
  • Memory and CPU usage
  • Last heartbeat

Storage Engines

SQL Databases

  • PostgreSQL - recommended for production
  • MySQL / MariaDB
  • Oracle
  • SQL Server
  • H2 - great for development
  • SQLite - embedded, zero-config
  • DB2, CockroachDB

NoSQL

  • MongoDB
  • Amazon DocumentDB
  • InMemory - testing only

Storage Engine Setup

// PostgreSQL
JobRunr.configure()
    .useStorageProvider(
        new PostgresStorageProvider(dataSource))

// SQLite
JobRunr.configure()
    .useStorageProvider(
        new SqLiteStorageProvider(dataSource))

// InMemory (testing)
JobRunr.configure()
    .useStorageProvider(
        new InMemoryStorageProvider())
  • Tables created automatically
  • All servers must share the same database

Spring Boot Integration

<dependency>
    <groupId>org.jobrunr</groupId>
    <artifactId>jobrunr-spring-boot-3-starter</artifactId>
    <version>8.4.2</version>
</dependency>
jobrunr.background-job-server.enabled=true
jobrunr.dashboard.enabled=true
  • Auto-configures with existing DataSource
  • Inject JobScheduler anywhere
  • @Recurring on Spring beans
  • Health actuator included

DEMO

example-spring

Distributed architecture with Spring Boot

^ Show multi-module setup. Start processingapp first (H2 TCP + BackgroundJobServer). Then webapp (enqueues jobs). Show @AsyncJob.


@AsyncJob - New in JobRunr v8

@Component
@AsyncJob
public class MyAsyncService {

    @Job(name = "my async spring job")
    public void simpleAsyncJob() {
        System.out.println("Auto-async!");
    }
}
// Just call the method - no JobScheduler needed!
myAsyncService.simpleAsyncJob();
// -> Automatically creates a background job

^ New in v8: annotate a class with @AsyncJob and method calls automatically become background jobs.


Quarkus Integration

<dependency>
    <groupId>org.jobrunr</groupId>
    <artifactId>quarkus-jobrunr</artifactId>
    <version>8.4.2</version>
</dependency>
quarkus.jobrunr.background-job-server.enabled=true
quarkus.jobrunr.dashboard.enabled=true
  • Quarkus extension (not just a library)
  • SmallRye Health integration
  • Micrometer metrics
  • Native image support with JobRequest pattern

DEMO

example-quarkus

Quarkus + @Recurring + JobRequest

^ Show @Recurring annotation, JobRequest pattern for native. Show dev mode with quarkus:dev.


MicroProfile / Jakarta EE Integration

@Produces @Singleton
public JobScheduler jobScheduler(
        StorageProvider storageProvider) {
    var scheduler = new JobScheduler(storageProvider);
    BackgroundJob.setJobScheduler(scheduler);
    return scheduler;
}

@Produces @Singleton
public JobActivator jobActivator() {
    return new JobActivator() {
        public <T> T activateJob(Class<T> type) {
            return CDI.current().select(type).get();
        }
    };
}

^ No starter available - wire it yourself with CDI producers. Full control over configuration.


DEMO

example-microprofile

CDI + OpenLiberty + Progress Tracking

^ Show JobRunrProvider.java CDI wiring. Call /jobrunr/start to start server. Enqueue long-running job with progress bar.


Distributed Processing

Web App (Server 1)  --enqueue-->  Shared DB (PostgreSQL)

Worker App (Server 2)  --process-->  Shared DB
Worker App (Server 3)  --process-->  Shared DB
  • Enqueue anywhere, process anywhere
  • Scale workers independently
  • All share the same database

Scaling - Adding Nodes

Just start another JVM pointing to the same database. Done.

# Same config on every node
spring.datasource.url=jdbc:postgresql://db-host:5432/mydb
jobrunr.background-job-server.enabled=true
jobrunr.background-job-server.worker-count=4
  • No registration, no discovery, no cluster config
  • Servers register themselves via heartbeat in jobrunr_backgroundjobservers
  • Jobs distributed via optimistic locking on DB rows
  • Need more throughput? Start another instance.

^ This is one of JobRunr's biggest strengths. The database IS the coordination layer. No ZooKeeper, no Redis, no message broker.


Automatic Retry Handling

  • Default: 10 retries with exponential backoff
  • 1st retry: ~15s, 2nd: ~30s, ... 10th: ~4.5h
  • Configurable per job via @Job(retries = 5)
  • Global config via properties
  • Failed jobs visible in dashboard with full stack trace
// Let JobRunr handle it - don't catch exceptions!
public void riskyJob() {
    externalApi.call(); // If this throws, JobRunr retries
}

^ Don't wrap your job in try-catch. Let exceptions bubble up and JobRunr handles retry logic for you.


Free vs Pro

Free (OSS)

  • Unlimited fire-and-forget & scheduled jobs
  • Up to 100 recurring jobs
  • Dashboard (standalone)
  • All storage engines
  • All framework integrations
  • Automatic retries

Pro

  • Unlimited recurring jobs
  • Priority queues & dynamic queues
  • Batches & job chaining
  • Server tags (route jobs to specific servers)
  • Rate limiting
  • Embedded dashboard + SSO
  • Observability (Micrometer + OpenTelemetry)
  • Real-time scheduling

Best Practices

  1. Pass IDs, not entities - data may be stale
  2. Keep job arguments simple - serialized to JSON
  3. Don't catch exceptions - let JobRunr retry
  4. Make jobs idempotent - they might run more than once
  5. Use @Job names - readable dashboard
  6. One BackgroundJobServer per JVM
  7. Same code version on all servers
  8. Use JobRequest for native images (GraalVM)

Resources


Thank You!

Questions?

Dominik Dorn

@domdorn

^ Thank you for listening! Happy to take questions. The example code is available at the links above.