Scheduling Spring Batch Jobs with Quartz (Clustered)
Fast recall
Quick revision
Core answer
qrtz_*), create a JobDetail + Trigger that calls JobLauncher.run(batchJob, params), enable clustering. Alternatives are ShedLock (lightweight DB lock atop @Scheduled) and Kubernetes CronJob (orchestration outside the app). For mature production Spring Batch, Quartz clustered mode plus JobLauncher is the standard combo.Scheduler selection, distributed-locking concern, Quartz clustering, Batch integration
Using `@Scheduled` in a multi-replica deployment; not enabling Quartz clustering
Compare Quartz vs ShedLock vs K8s CronJob; discuss misfire policies, thread-pool sizing, JobParameters uniqueness, and `requestsRecovery` for node-failure handling
Interview-ready guide
Interview answer
Spring's @Scheduled works for single-instance deployments, but in a cluster every replica fires the trigger — you need a scheduler with distributed locking.
Quartz clustered mode is the standard answer:
- JDBC-backed JobStore with
org.quartz.jobStore.isClustered=true - All nodes share the same Quartz schema
- One node grabs the trigger row lock and runs the job
- Bridge to Spring Batch via
QuartzJobBeancallingJobLauncher.run(job, params) - Always include a unique parameter (timestamp or
RunIdIncrementer) so each fire creates a newJobInstance
ShedLock is the lightweight alternative:
@Scheduledplus@SchedulerLock(name, lockAtMostFor, lockAtLeastFor)- Backed by a
LockProvider(JDBC, Redis, ZooKeeper) - Only one node holds the named lock at a time
- Use when you want simplicity over Quartz's rich features
Kubernetes CronJob is the operational alternative:
- K8s schedules a fresh Pod at each tick
- Pod runs the Boot app, runs the batch job, exits
- No scheduler inside the application
- Use when you're K8s-native and want operational separation
Production practices:
- Apply Quartz DDL via Flyway, not
initialize-schema - Set explicit misfire policies —
fire-and-proceedfor daily reports,do-nothingfor high-frequency syncs - Size the Quartz thread pool to peak job concurrency
- Set
requestRecovery(true)on JobDetails so node crashes don't orphan work - Make Batch jobs idempotent — recovery may re-run them
- Emit Micrometer metrics for misfires, durations, in-flight jobs
Anti-patterns to call out:
@Scheduledin a multi-replica deployment- Same
JobParametersevery run (JobInstanceAlreadyCompleteException) initialize-schema: alwaysin production- Long-running jobs starving the Quartz thread pool
In an interview I'd emphasize that the choice is operational, not technical — Quartz, ShedLock, and K8s CronJob all work; pick by team expertise and infrastructure context.
Scheduling Spring Batch Jobs with Quartz (Clustered)
Direct Answer
Spring's @Scheduled is fine for a single instance, but in multi-node deployments every pod fires the same job — you need a scheduler with distributed locking. Quartz supports this via a JDBC-backed JobStore with org.quartz.jobStore.isClustered=true — DB row locks ensure only one node picks up each trigger. The integration pattern: configure Quartz tables (qrtz_*), create a JobDetail + Trigger that calls JobLauncher.run(batchJob, params), enable clustering. Alternatives are ShedLock (lightweight DB lock atop @Scheduled) and Kubernetes CronJob (orchestration outside the app). For mature production Spring Batch, Quartz clustered mode plus JobLauncher is the standard combo.
Common Mistakes & Pitfalls
Using @Scheduled in a multi-replica deployment; not enabling Quartz clustering
Check your understanding
Answer these without looking back at the article.
- How does Quartz clustering actually guarantee only one node runs a trigger?
- What's the difference between misfire policies fire-and-proceed and do-nothing?
- When would you prefer ShedLock over Quartz?
- How do you ensure idempotency when a node crashes mid-job and Quartz recovers it?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.