Comparison

Spring @Scheduled vs Quartz.

@Scheduled is the lightweight built-in option — annotation on a method, runs in your Spring app's thread pool. Quartz is a separate library with database-backed persistence, clustering, and richer cron syntax. Use @Scheduled for simple cases; reach for Quartz when you need durability or distribution.

Side by side

Feature@ScheduledQuartz
SetupAnnotation, zero depsLibrary + (optionally) DB schema
Cron syntax6-field (with seconds)6-7 fields + L, W, # operators
PersistenceNone (in-memory)JDBC store (jobs survive restarts)
ClusteringNone (runs on every node)Yes (only one node fires each trigger)
Dynamic schedulesLimited (recompile or SpEL)Yes (runtime API)
Misfire handlingNoneConfigurable policies
Best forSingle-instance apps, cron-styleMulti-instance apps, complex schedules

@Scheduled basics

@Configuration
@EnableScheduling
public class AppConfig { }

@Service
public class ReportService {
    @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "America/New_York")
    public void sendDailyReport() {
        // ...
    }
}

That's it. Spring creates a single-threaded scheduler by default; configure a pool if you have many concurrent jobs.

Quartz basics

JobDetail job = JobBuilder.newJob(ReportJob.class)
    .withIdentity("dailyReport", "reports")
    .build();

Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("dailyReportTrigger")
    .withSchedule(CronScheduleBuilder
        .cronSchedule("0 0 9 ? * MON-FRI")
        .inTimeZone(TimeZone.getTimeZone("America/New_York")))
    .build();

scheduler.scheduleJob(job, trigger);

More boilerplate, but you can store jobs in a database and define triggers at runtime instead of compile time.

The clustering problem

If you deploy a Spring app with @Scheduled to three Kubernetes pods, the cron job runs three times — once per pod. That's usually wrong.

Quartz with a JDBC job store can be configured to run each trigger on exactly one node, using database row-locking as the coordination mechanism. This is the killer feature.

Without Quartz, alternatives include:

  • ShedLock — adds locking to @Scheduled without a full Quartz install
  • Leader election (Spring Cloud) — only the elected leader runs scheduled jobs
  • Designate one pod as "the scheduler" via deployment topology

Persistence

With @Scheduled, jobs live in memory. Restart the app: jobs are reconstituted from annotations, but any dynamically created schedules are lost.

Quartz with a JDBC store survives restarts. Schedules created via API persist across deploys. This matters for user-facing scheduling features ("notify me at 9 AM next Tuesday") that you'd otherwise have to manage in your own tables.

Cron syntax differences

Spring @Scheduled cron syntax: 6 fields starting with seconds. Day-of-week 1=Mon (Linux-style, with 0/7=Sunday).

Quartz cron syntax: 6-7 fields. Day-of-week 1=Sunday (offset by one from Linux). Supports L (last), W (nearest weekday), # (nth weekday).

So 0 0 9 ? * MON-FRI works in both (using named days). But 0 0 9 ? * 2-6 means Mon-Fri in Quartz, Tue-Sat in Spring. Always use named days for portability.

When to use which

If you have…Use…
Single-instance app, fixed schedule@Scheduled
Single instance, complex schedule (L/W/#)Quartz (without JDBC)
Multi-instance app, fixed schedule@Scheduled + ShedLock
Multi-instance app, dynamic schedulesQuartz with JDBC
User-facing scheduling featuresQuartz with JDBC
Related

Continue reading.