Quartz Scheduler setup, integration with Spring Boot, deploy using kubernates, a simple Hands-on
This article is about how to setup and integrate quartz scheduler in a Spring Boot application with simple setup, deploy using Kubernates. Quartz Scheduler is an open- source Scheduling framework for Java. Below are some of the main features of Quartz which will be focused in this article. Job Schedule/Execution/Persistence Clustering. Prerequisites: Below are the dependencies for this demo. Used https://start.spring.io/ to create the Spring Boot application. JDK 17 Gradle-Groovy Quartz Scheduler PostgreSQL Driver.(Using it to persist Job info) Lombok Below is the project folder structure. Code Setup: Follow the below steps for code setup.. Download/Generate the Spring boot project with prerequisites mentioned above and import it to any IDE. In this case I am using IntelliJ build.gradlefile should be similar to below. plugins { id 'java' id 'org.springframework.boot' version '3.4.4' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.shastry.learning.quartz' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-quartz' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() } Create couple of jobs which will be executed by Quartz. @DisallowConcurrentExecution will prevent running concurrent execution of job. Meaning only one Job of this type be executed at any time. @PersistJobDataAfterExecution will persist the Job data once execution is completed. package com.shastry.learning.quartz.QuartzDemo.scheduled; import lombok.extern.slf4j.Slf4j; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution; import java.time.OffsetDateTime; @Slf4j @DisallowConcurrentExecution @PersistJobDataAfterExecution public class OneMinuteJob implements Job { /** * @param context * @throws JobExecutionException */ @Override public void execute(JobExecutionContext context) throws JobExecutionException { log.info("Executing OneMinuteJob at {}", OffsetDateTime.now().toInstant()); } } package com.shastry.learning.quartz.QuartzDemo.scheduled; import lombok.extern.slf4j.Slf4j; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.PersistJobDataAfterExecution; import java.time.OffsetDateTime; @Slf4j @DisallowConcurrentExecution @PersistJobDataAfterExecution public class ThirtySecondJob implements Job { /** * @param context * @throws JobExecutionException */ @Override public void execute(JobExecutionContext context) throws JobExecutionException { log.info("Executing ThirtySecondJob at {}", OffsetDateTime.now().toInstant()); } } Configure AutowiringSpringBeanFactory This config ensures Quartz to create job instances using Spring’s context with required dependencies. package com.shastry.learning.quartz.QuartzDemo.config; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.quartz.SpringBeanJobFactory; public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private transient AutowireCapableBeanFactory beanFactory; @Override public void setApplicationContext(final ApplicationContext context) { beanFactory = context.getAutowireCapableBeanFactory(); } @Override protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { final Object job = super.createJobInstance(bundle); beanFactory.autowireBean(job); return job; } } Configure Quartz Scheduler. SchedulerConfig configures Jobs, triggers and schedulers. Different type of triggers are used for different jobs Simple Trigger: This can be used to fire jobs at regular interval of time repeatedly. The trigger can be configured to start after some fixed delay. Here 30s job is configured using SimpleTrigger. Cron Trigger: This can be used for creating triggers based on a cron expression. Here 1min job is configured using CRON expression. package com.shastry.learning.quartz.QuartzDemo.config; import com.shastry.learning.quartz.Qu

This article is about how to setup and integrate quartz scheduler in a Spring Boot application with simple setup, deploy using Kubernates.
Quartz Scheduler is an open- source Scheduling framework for Java. Below are some of the main features of Quartz which will be focused in this article.
- Job Schedule/Execution/Persistence
- Clustering.
Prerequisites:
Below are the dependencies for this demo. Used https://start.spring.io/ to create the Spring Boot application.
- JDK 17
- Gradle-Groovy
- Quartz Scheduler
- PostgreSQL Driver.(Using it to persist Job info)
- Lombok
Below is the project folder structure.
Code Setup:
Follow the below steps for code setup..
- Download/Generate the Spring boot project with prerequisites mentioned above and import it to any IDE. In this case I am using IntelliJ
- build.gradlefile should be similar to below.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.4'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.shastry.learning.quartz'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-quartz'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
- Create couple of jobs which will be executed by Quartz.
@DisallowConcurrentExecution
will prevent running concurrent execution of job. Meaning only one Job of this type be executed at any time.@PersistJobDataAfterExecution
will persist the Job data once execution is completed.
package com.shastry.learning.quartz.QuartzDemo.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import java.time.OffsetDateTime;
@Slf4j
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class OneMinuteJob implements Job {
/**
* @param context
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("Executing OneMinuteJob at {}", OffsetDateTime.now().toInstant());
}
}
package com.shastry.learning.quartz.QuartzDemo.scheduled;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import java.time.OffsetDateTime;
@Slf4j
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class ThirtySecondJob implements Job {
/**
* @param context
* @throws JobExecutionException
*/
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("Executing ThirtySecondJob at {}", OffsetDateTime.now().toInstant());
}
}
- Configure
AutowiringSpringBeanFactory
This config ensures Quartz to create job instances using Spring’s context with required dependencies.
package com.shastry.learning.quartz.QuartzDemo.config;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
- Configure Quartz Scheduler.
SchedulerConfig
configures Jobs, triggers and schedulers. Different type of triggers are used for different jobs
- Simple Trigger: This can be used to fire jobs at regular interval of time repeatedly. The trigger can be configured to start after some fixed delay. Here 30s job is configured using SimpleTrigger.
- Cron Trigger: This can be used for creating triggers based on a cron expression. Here 1min job is configured using CRON expression.
package com.shastry.learning.quartz.QuartzDemo.config;
import com.shastry.learning.quartz.QuartzDemo.scheduled.OneMinuteJob;
import com.shastry.learning.quartz.QuartzDemo.scheduled.ThirtySecondJob;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.Trigger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSourceScriptDatabaseInitializer;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.List;
import java.util.Properties;
@Configuration
@AllArgsConstructor
public class SchedulerConfig {
@Component
@Data
@ConfigurationProperties("app.scheduling")
public static class SchedulerConfigProperties {
@Data
public static class ScheduledJobTimingConfig {
private String cron;
private int startDelayMillis;
private int fixedDelay;
}
private ScheduledJobTimingConfig oneMinuteJob;
private ScheduledJobTimingConfig thirtySecondJob;
}
private SchedulerConfigProperties schedulerConfigProperties;
private final QuartzProperties quartzProperties;
// --------------------- Jobs -----------------------------
@Bean("oneMinuteJob")
public JobDetailFactoryBean oneMinuteJob() {
return createJobDetail(OneMinuteJob.class, "One Minute Job");
}
@Bean("thirtySecondJob")
public JobDetailFactoryBean thirtySecondJob() {
return createJobDetail(ThirtySecondJob.class, "Thirty Second Job");
}
// --------------------- Triggers ------------------------
@Bean("thirtySecondJobTrigger")
public SimpleTriggerFactoryBean thirtySecondJobTrigger(@Qualifier("thirtySecondJob") JobDetail jobDetail) {
var trigger = new SimpleTriggerFactoryBean();
trigger.setJobDetail(jobDetail);
trigger.setName(jobDetail.getDescription());
trigger.setStartDelay(schedulerConfigProperties.getThirtySecondJob().getStartDelayMillis());
trigger.setRepeatInterval(schedulerConfigProperties.getThirtySecondJob().getFixedDelay());
return trigger;
}
@Bean("oneMinuteJobTrigger")
public CronTriggerFactoryBean oneMinuteJobTrigger(@Qualifier("oneMinuteJob") JobDetail jobDetail) {
return createCronTrigger(jobDetail,
schedulerConfigProperties.getOneMinuteJob().getCron(),
schedulerConfigProperties.getOneMinuteJob().getStartDelayMillis());
}
@Bean
@DependsOn("quartzDataSourceInitializer")
public SchedulerFactoryBean schedulerFactoryBean(
List jobDetails,
List triggers,
DataSource dataSource,
DataSourceTransactionManager transactionManager) {
var props = new Properties();
var factory = new SchedulerFactoryBean();
props.putAll(quartzProperties.getProperties());
factory.setQuartzProperties(props);
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setJobFactory(autowiringSpringBeanJobFactory());
factory.setJobDetails(jobDetails.toArray(new JobDetail[0]));
factory.setTriggers(triggers.toArray(new Trigger[0]));
factory.setAutoStartup(true);
factory.setOverwriteExistingJobs(true);
return factory;
}
@Bean
public QuartzDataSourceScriptDatabaseInitializer quartzDataSourceInitializer(DataSource dataSource) {
return new QuartzDataSourceScriptDatabaseInitializer(dataSource, quartzProperties);
}
@Bean
public AutowiringSpringBeanJobFactory autowiringSpringBeanJobFactory() {
return new AutowiringSpringBeanJobFactory();
}
private JobDetailFactoryBean createJobDetail(Class jobClass, String jobName) {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(jobClass);
jobDetailFactoryBean.setDescription(jobName);
jobDetailFactoryBean.setDurability(true);
return jobDetailFactoryBean;
}
private CronTriggerFactoryBean createCronTrigger(JobDetail jobDetail, String cron, int startDelayMillis) {
CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
cronTriggerFactoryBean.setJobDetail(jobDetail);
cronTriggerFactoryBean.setCronExpression(cron);
cronTriggerFactoryBean.setStartDelay(startDelayMillis);
cronTriggerFactoryBean.setName(jobDetail.getDescription());
return cronTriggerFactoryBean;
}
}
- Create
application.yml
to configure db, quartz and schedulers.
spring:
application:
name=QuartzDemo
datasource:
url: jdbc:postgresql://quartz-postgres-service:5432/postgres
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
jpa:
show-sql: false
hibernate:
ddl-auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always
properties:
org:
quartz:
jobStore:
clusterCheckinInterval: 20000
misfireThreshold: 60000
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate #Postgre specific
scheduler:
instanceName: quartz-demo
instanceId: AUTO
threadPool:
threadCount: 10
threadPriority: 5
class: org.quartz.simpl.SimpleThreadPool
app:
scheduling:
oneMinuteJob:
cron: "0 0/1 * * * ? *" # Cron expression to run every minute
start-delay-millis: 10000 # Start the job after the configured delay
thirtySecondJob:
fixed-delay: 30000 # Run/Repeat the job every 30 seconds
start-delay-millis: 20000
Note:
postgresql hostnames are configured in kubernates config which will be covered in the deployment section.
Deployment:
- Generate Dockerfile
# Java 17 base image for the app
FROM openjdk:17-jdk-slim-buster
LABEL authors="sumanth.shastry"
# Setup base dir for the app to run
WORKDIR /app
# Copy the app jar
COPY build/libs/QuartzDemo*.jar /app/quartz-demo.jar
ENTRYPOINT ["java", "-jar", "/app/quartz-demo.jar"]
- Create Persistent Volume and claim for PostgreSQL
apiVersion: v1
kind: PersistentVolume
metadata:
name: quartz-postgres-pv
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data/quartz-postgres"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: quartz-postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
- Create ConfigMap to configure PostgreSQL DB details
apiVersion: v1
kind: ConfigMap
metadata:
name: quartz-postgres-config
data:
POSTGRES_DB: "postgres"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
Create services for PostgreSQL and spring boot app.
apiVersion: v1
kind: Service
metadata:
name: quartz-postgres-service
spec:
selector:
app: quartz-postgres-service
ports:
- protocol: TCP
port: 5432
targetPort: 5432
nodePort: 31079
type: NodePort
---
apiVersion: v1
kind: Service
metadata:
name: quartz-demo
spec:
selector:
app: quartz-demo
ports:
- port: 8080
targetPort: 8080
nodePort: 30080
protocol: TCP
type: NodePort
- Create deployment forPostgreSQL and spring boot app.
Note: Created 2 replicas for Spring Boot app to test Load Balancing and no overlap/concurrent jobs running in separete instances.
apiVersion: apps/v1
kind: Deployment
metadata:
name: quartz-postgres-service
labels:
app: quartz-postgres-service
spec:
replicas: 1
selector:
matchLabels:
app: quartz-postgres-service
template:
metadata:
name: quartz-postgres-service
labels:
app: quartz-postgres-service
spec:
containers:
- name: quartz-postgres-service
image: postgres:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
protocol: TCP
envFrom:
- configMapRef:
name: quartz-postgres-config
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-storage
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: quartz-postgres-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: quartz-demo
labels:
app: quartz-demo
spec:
replicas: 2
selector:
matchLabels:
app: quartz-demo
template:
metadata:
name: quartz-demo
labels:
app: quartz-demo
spec:
containers:
- name: quartz-demo
image: quartz-demo:0.0.1-SNAPSHOT
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
protocol: TCP
- Create deployment script to build spring boot, create docker image create PostgreSQL using above k8s config.
#!/bin/bash
APP_NAME="quartz-demo"
VERSION="0.0.1-SNAPSHOT"
DOCKER_IMAGE="${APP_NAME}:${VERSION}"
NAMESPACE="quartz-namespace"
# Build the application
echo "Building the application..."
./gradlew clean build
# Generate Docker image
echo "Building Docker image..."
docker build -t ${DOCKER_IMAGE} .
# Clean up Kubernetes deployment
echo "Cleaning up Kubernetes deployment..."
kubectl delete -f k8s/quartz-demo-configmap.yaml -n ${NAMESPACE}
kubectl delete -f k8s/quartz-demo-service.yaml -n ${NAMESPACE}
kubectl delete -f k8s/quartz-demo-deployment.yaml -n ${NAMESPACE}
kubectl delete -f k8s/quartz-demo-pv.yaml -n ${NAMESPACE}
kubectl delete namespace ${NAMESPACE}
# Apply Kubernetes configurations
echo "Deploying application to Kubernetes..."
kubectl create namespace ${NAMESPACE}
kubectl apply -f k8s/quartz-demo-configmap.yaml -n ${NAMESPACE}
kubectl apply -f k8s/quartz-demo-pv.yaml -n ${NAMESPACE}
kubectl apply -f k8s/quartz-demo-service.yaml -n ${NAMESPACE}
kubectl apply -f k8s/quartz-demo-deployment.yaml -n ${NAMESPACE}
echo "Deployment complete."
Testing:
Once build script is executed, 2 instances would be running. Below snapshot of logs shows same job is not executed 2 instances. Jobs execution is load balanced.
Sample log:
2025-03-28T06:59:00.006Z INFO 1 --- [-demo_Worker-10] c.s.l.q.Q.scheduled.OneMinuteJob : Executing OneMinuteJob at 2025-03-28T06:59:00.006097128Z
2025-03-28T07:00:05.766Z INFO 1 --- [z-demo_Worker-1] c.s.l.q.Q.scheduled.ThirtySecondJob : Executing ThirtySecondJob at 2025-03-28T07:00:05.766212130Z
Conclusion:
In this article, we learned a simple, easy hands-on related to integrating Quartz framework with Spring boot.
Find the respective code over the GitHub.
References:
https://github.com/quartz-scheduler/quartz/blob/main/docs/introduction.adoc