Java Quartz Scheduler — A Complete Guide
Quartz is Java's enterprise-grade scheduler — used across the JVM ecosystem by Apache Camel, Talend, Camunda, and Spring's Quartz integration. This guide covers Quartz beyond the dialect page: setting up the Scheduler, the difference between SimpleTrigger and CronTrigger, JobDataMap for parameterizing jobs, JDBC job stores for persistence, clustering, and the Spring Boot integration.
Updated
The Quartz model
Quartz separates what to run from when to run it:
- Job — a class implementing
org.quartz.Jobwith one method,execute(JobExecutionContext). Defines what work to do. - JobDetail — an instance of a Job with associated configuration (durability, requests recovery, JobDataMap).
- Trigger — defines when the Job fires. Two main types:
SimpleTrigger(fixed start + interval) andCronTrigger(cron expression). - Scheduler — the runtime that manages JobDetails and Triggers, fires Jobs at the right time.
The “Hello world” of Quartz:
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "group1")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 */5 * * * ?"))
.build();
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
scheduler.scheduleJob(job, trigger);
This conceptual separation matters in practice: one Job class can be triggered by many different Triggers, and one Trigger fires exactly one JobDetail. Parameterize with JobDataMap (covered below) to keep Job classes generic.
Setting up the Scheduler
First, add Quartz to your project. The current stable release is 2.5.2 (December 2025):
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.5.2</version>
</dependency>
// Gradle
implementation 'org.quartz-scheduler:quartz:2.5.2'
Spring Boot users should skip the raw dependency and use spring-boot-starter-quartz instead (see Spring Boot integration below) — it pulls in a Boot-managed Quartz version for you.
Quartz reads configuration from quartz.properties on the classpath (or programmatically via Properties + StdSchedulerFactory).
Minimal quartz.properties for in-memory operation:
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
For JDBC-backed (production):
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.dataSource.myDS.driver = org.postgresql.Driver
org.quartz.dataSource.myDS.URL = jdbc:postgresql://localhost/myapp
org.quartz.dataSource.myDS.user = myuser
org.quartz.dataSource.myDS.password = mypass
org.quartz.dataSource.myDS.maxConnections = 5
The Quartz table schema lives in org.quartz.impl.jdbcjobstore.tables_*.sql for each supported database. Run the appropriate one before starting the scheduler.
Jobs and Triggers
A Quartz Job is a class with one method:
public class DailyReportJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// ... do work
}
}
Two main Trigger types:
SimpleTrigger — fixed start + interval:
SimpleTrigger every5Min = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever())
.build();
CronTrigger — cron expression:
CronTrigger weekdayMornings = TriggerBuilder.newTrigger()
.withIdentity("trigger2", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 ? * MON-FRI")
.inTimeZone(TimeZone.getTimeZone("America/New_York")))
.build();
The cron syntax here is Quartz’s — see the Quartz dialect reference. Note the mandatory ? placeholder when day-of-month and day-of-week would otherwise both be specific.
Schedule a job with a trigger:
scheduler.scheduleJob(jobDetail, trigger);
A JobDetail can have multiple Triggers — fire the same job at different times:
Set<Trigger> triggers = new HashSet<>();
triggers.add(trigger1);
triggers.add(trigger2);
scheduler.scheduleJob(jobDetail, triggers, true); // replace = true
JobDataMap
JobDataMap is how you parameterize Jobs. It’s a serializable Map<String, Object> attached to either the JobDetail or the Trigger (or both — they merge).
JobDetail job = JobBuilder.newJob(SendEmailJob.class)
.withIdentity("sendEmail", "group1")
.usingJobData("recipient", "team@example.com")
.usingJobData("subject", "Daily report")
.build();
// Or per-trigger
Trigger triggerForCustomerA = TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 * * ?"))
.usingJobData("customerId", "A123")
.build();
Inside the Job:
public class SendEmailJob implements Job {
public void execute(JobExecutionContext context) {
JobDataMap data = context.getMergedJobDataMap();
String recipient = data.getString("recipient");
String customerId = data.getString("customerId");
// ...
}
}
Use JobDataMap to keep Job classes generic and reusable. One SendEmailJob class with many JobDetail + Trigger combos beats one Job class per email type.
JDBC job stores
The default RAMJobStore is simplest but loses everything on restart. For production, use JobStoreTX (or JobStoreCMT if your container manages transactions).
Setup:
- Run the Quartz table schema for your database (PostgreSQL, MySQL, Oracle, SQL Server, etc.). The SQL scripts ship with Quartz under
org/quartz/impl/jdbcjobstore/. - Configure
quartz.propertieswithjobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX. - Configure a
dataSourceQuartz can use.
Once on JDBCJobStore:
- Scheduled jobs and triggers persist across restarts
- Job execution history is queryable (via Quartz’s history extensions or your own queries on the QRTZ_ tables)
- You can change schedules at runtime and they survive restart
- You can enable clustering
The Quartz table footprint is ~11 tables (QRTZ_JOB_DETAILS, QRTZ_TRIGGERS, QRTZ_CRON_TRIGGERS, etc.). They’re managed by Quartz — don’t write to them directly.
Clustering
Run multiple Quartz Scheduler instances against the same JDBCJobStore database. Each instance gets a unique instanceId; they coordinate via row-level locks on the QRTZ_ tables.
org.quartz.scheduler.instanceId = AUTO
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
What clustering gives you:
- Failover — if one instance dies mid-job, another instance recovers and re-runs (if
requestRecovery = trueon the JobDetail) - Load balancing — triggered jobs go to whichever instance picks them up first
- No single point of failure for the scheduler itself
Caveats:
- All instances must use the same database
- Job classes must be available on every instance
- Clock skew between instances can cause subtle timing issues — keep them NTP-synced
RAMJobStoredoesn’t support clustering — JDBC only
For multi-region deployments, run separate Quartz clusters per region and partition your schedules accordingly.
Spring Boot integration
Spring Boot has a Quartz auto-configuration. Add the starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
Configure in application.yml:
spring:
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: never # see the warning below
properties:
org:
quartz:
scheduler.instanceId: AUTO
jobStore.isClustered: true
Don’t use
initialize-schema: alwaysin production. Quartz’s bundled DDL scripts begin withDROP TABLE, soalwaysre-runs them on every startup and wipes all persisted jobs and triggers — exactly what the JDBC store and clustering are meant to protect. Useneverand create the schema out-of-band (a migration tool like Flyway/Liquibase, or run thetables_*.sqlscript once).embeddedis fine for throwaway in-memory databases only.
Define jobs as Spring beans:
@Component
public class DailyReportJob extends QuartzJobBean {
@Autowired private ReportService reportService;
@Override
protected void executeInternal(JobExecutionContext context) {
reportService.generateDaily();
}
}
Then declare the JobDetail + Trigger as Spring beans:
@Configuration
public class QuartzConfig {
@Bean
public JobDetail dailyReportJobDetail() {
return JobBuilder.newJob(DailyReportJob.class)
.withIdentity("dailyReport")
.storeDurably()
.build();
}
@Bean
public Trigger dailyReportTrigger(JobDetail dailyReportJobDetail) {
return TriggerBuilder.newTrigger()
.forJob(dailyReportJobDetail)
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 9 ? * MON-FRI"))
.build();
}
}
This integration is the cleanest path for Spring Boot apps that want Quartz — Spring handles the DataSource, configures Quartz to use Spring’s bean factory for job instantiation (so @Autowired works in Jobs), and initializes the schema on startup.
For simpler scheduling that doesn’t need Quartz’s features, see Spring @Scheduled instead.
Misfire handling
A misfire happens when a Trigger should have fired but didn’t (scheduler down, thread pool exhausted, JDBCJobStore lock contention). Quartz has policies for what to do.
For CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 9 ? * MON-FRI")
.withMisfireHandlingInstructionDoNothing(); // skip the missed fire
// or
.withMisfireHandlingInstructionFireAndProceed(); // fire once immediately, then resume schedule
// or
.withMisfireHandlingInstructionIgnoreMisfires(); // fire all missed (potentially many) ASAP
For SimpleTrigger:
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever()
.withMisfireHandlingInstructionNextWithRemainingCount();
// or NextWithExistingCount, NowWithExistingCount, NowWithRemainingCount,
// IgnoreMisfires, FireNow
The defaults are usually wrong for production:
- For daily jobs, you want
DoNothing(don’t fire 10 hours late) - For high-frequency jobs, you want
IgnoreMisfiresorNowWithExistingCount(don’t queue up hundreds of missed fires)
Set the misfire policy explicitly on every Trigger. The miss-threshold itself is org.quartz.jobStore.misfireThreshold (default 60000ms = 60 seconds — Triggers more than 60s late are considered “misfired”).
Frequently asked questions
When should I use Quartz instead of Spring @Scheduled?
What's the difference between SimpleTrigger and CronTrigger?
How does JobDataMap work?
Do I need a database for Quartz?
How does Quartz clustering work?
Related
Every 5 Minutes
Quartz equivalent: `0 */5 * * * ?`
ToolCron Parser
Verify Quartz expressions with the dialect selector.
GuideSpring @Scheduled
The simpler Spring-native scheduling alternative.
Guide.NET / C# Cron
Quartz.NET ports this same API to the .NET ecosystem.
GuideCron Dialect Comparison
How Quartz syntax differs from Unix, Spring, and others.