Creating Grails Scheduled Jobs With the Quartz Plugin

If you have ever built a Grails application that needs to run a report at 3am, send a reminder email every Monday morning, or tidy up stale records overnight, you have probably bumped into the same wall many Aussie developers face: how do you reliably schedule background work without rolling your own thread pool? The good news is that Grails sits on top of Spring, and Spring has a long-standing friendship with the Quartz scheduler. By installing the Quartz plugin, you get cron-style triggers, persistent job stores, clustering support, and a clean Groovy DSL to wire everything up.

This article walks through the practical steps of adding scheduled jobs to a Grails application, from a fresh install on your laptop to a production deployment running on a cloud instance in a Sydney data centre. Along the way, you will see how to write jobs in Groovy, how to express schedules using cron expressions, and how to deal with the quirks of Australian time zones, including daylight saving in New South Wales, Victoria, Tasmania, and the ACT. No prior Quartz experience is required, though a bit of familiarity with Grails services will help.

We will also touch on monitoring, error handling, and a few performance tips that pair nicely with the broader topic of keeping a Grails app fast. If you are new to the framework, the rest of the site has plenty of beginner-friendly tutorials to get you started before you dive into scheduled work.

Setting Up the Quartz Plugin in Your Grails Application

The first step is to add the plugin to your build.gradle file. The Quartz plugin for Grails wraps the underlying scheduler and exposes a Grails-friendly configuration block in application.yml. After adding the dependency, a quick ./gradlew refresh-dependencies is usually enough to pull everything down. In a typical Melbourne or Sydney dev shop, you would then run ./gradlew run-app to confirm that the scheduler starts up cleanly and logs a line similar to Quartz Scheduler v2.x started.

Because the plugin ships with sensible defaults, you do not need to write any code to get a basic scheduler running. However, in production you will want to switch from the in-memory job store to a JDBC-backed store, so jobs survive an application restart. The plugin documentation shows how to point Quartz at your existing database, whether that is Postgres on RDS in ap-southeast-2 or MySQL sitting on a dedicated host. Once the tables are created, the scheduler will pick them up automatically.

A neat trick is to keep all of your scheduled jobs inside a grails-app/jobs directory. The plugin picks up any class ending in Job and registers it, which means you can drop a new file in, restart the app, and see the job appear in the logs. This convention keeps the code discoverable for the next developer who joins the team, which is especially handy in rotating on-call rosters common in Australian fintechs and government projects.

Defining Your First Job and Trigger

A Quartz job in Grails is just a Groovy class that extends grails.plugins.quartz.Job or implements the standard org.quartz.Job interface. The execute method receives a JobExecutionContext and contains the actual work, such as calling a service, hitting an external API, or writing to a database. Most developers keep the body of the job thin and delegate the heavy lifting to a service, which makes the job easy to unit test and reuse outside of the scheduler.

Triggers are where the real configuration happens. You can define them in application.yml using the Quartz plugin's DSL, where each job gets a name, a group, and a schedule expression. A simple simple trigger fires every thirty seconds, while a cron trigger lets you write expressions like 0 0 9 ? * MON to run something every Monday at 9am. Many teams in Brisbane and Perth use cron triggers to align jobs with local business hours, since customers expect reminders and reports to land while they are at their desks.

Common trigger patterns worth knowing:

Once your job and trigger are defined, you can start the scheduler in BootStrap.groovy or rely on the auto-start behaviour that the plugin provides. Either way, the first time your job runs, you should see a log line with the job name, the trigger name, and the fire time, which makes debugging much easier when something does not behave as expected.

Configuring Cron-Style Schedules for Australian Business Hours

Cron expressions look cryptic at first, but they break down into five fields: seconds, minutes, hours, day of month, month, and day of week. The wildcards and ranges give you enormous flexibility, which is why cron is the go-to choice for most production schedules. The trick is to remember that Quartz uses its own cron format rather than the Unix one, so ? is used to leave the day-of-week or day-of-month field empty instead of *.

A few Australian-friendly examples make this concrete. To run a job at 8:30am every weekday in Sydney, you would write 0 30 8 ? * MON-FRI. To fire a job at 6pm AEST on the last day of the month, you would write 0 0 18 L * ?. If your team works across states, you might stagger schedules so a Perth-based job runs an hour and a half earlier than its Melbourne counterpart, avoiding overlap on shared downstream systems.

It is also worth thinking about public holidays. Australia Day, ANZAC Day, and state-specific days such as Labour Day in Melbourne or the Royal Queensland Show in Brisbane can change when business processes actually run. Rather than hard-coding a calendar inside your application, a common pattern is to maintain a simple holiday table in your database and let your job check it before doing meaningful work. This keeps the schedule clean while still respecting the cultural rhythms of the local market.

Handling Time Zones and Daylight Saving the Smart Way

Time zones are the single biggest source of bugs in any scheduled system, and Australia is particularly tricky because four states observe daylight saving while Western Australia, Queensland, and the Northern Territory do not. If you hard-code Australia/Sydney in a job that runs in a data centre hosted overseas, you may end up with an hour of drift or a job that fires twice on the spring-forward weekend.

The fix is to always store schedules in UTC internally and let Quartz convert to a named zone at trigger time. In application.yml, you can set the timeZone property on each trigger to a value like Australia/Sydney or Australia/Perth. Quartz will then evaluate the cron expression in that zone, including the correct daylight saving transitions. For teams supporting customers across the country, this approach also makes it easy to add a new region later, since the schedule is just data rather than code.

If your jobs need to fire relative to the user's local time, a useful pattern is to store the user's time zone on their profile and use a per-user trigger created programmatically. This is more common in consumer-facing apps, such as a booking platform that wants to send a reminder an hour before a class in Adelaide regardless of where the server is hosted. The Quartz plugin exposes the underlying scheduler bean, so you can call scheduleJob from a service whenever a new booking is made.

Persisting Job State and Recovering After a Crash

Outages happen, whether it is a network blip in a Sydney cloud region or a routine redeploy of your application. When the scheduler comes back up, you want it to know which jobs were in flight and either rerun them or skip them cleanly. The JDBC job store makes this straightforward, since Quartz records the state of every trigger in a set of tables such as qrtz_job_details, qrtz_triggers, and qrtz_fired_triggers.

To make the most of this, configure the misfireInstruction on each trigger. The most common choices are MISFIRE_INSTRUCTION_FIRE_ONCE_NOW for jobs that should run once they can, and MISFIRE_INSTRUCTION_DO_NOTHING for jobs where missing a window is preferable to running late. For example, a monthly billing job should usually fire once as soon as the scheduler is healthy again, while a daily summary email is often better skipped than sent a day late.

You can also wrap the body of each job in a try-catch that logs the failure with enough context to diagnose it. Pair this with a simple counter table that tracks how many times a job has failed, and you have the bones of a self-healing scheduler. Many Australian engineering teams surface this counter in a Grafana dashboard so they can spot a flaky job before it becomes a customer-facing incident.

Monitoring and Performance Tuning

Once your jobs are running, the next question is how to know they are healthy. The Quartz plugin exposes a quartzScheduler bean that lets you query the scheduler for the current state of every trigger. A lightweight admin page can list each job, its last fire time, its next fire time, and whether it is currently running. This is useful during business hours in Brisbane or Canberra when someone wants to confirm that the 9am report has actually gone out.

From a performance angle, scheduled jobs share the same heap as the rest of your Grails application, so a runaway job can starve request-handling threads. A useful habit is to run jobs on a separate thread pool by setting the threadPool.threadCount property in application.yml. For most teams, a pool size between five and ten is plenty, but heavy ETL jobs may need more. If you are tuning the wider application, the profiling walkthrough walks through how to identify bottlenecks that show up only under scheduled load.

Useful metrics to track over time:

With these signals in place, you can tune cron expressions, adjust thread pool sizes, and decide which jobs should run in a separate worker process. Over a few weeks, you will have a schedule that is both predictable and forgiving, which is exactly what you want when a customer at 7am in Perth opens their phone and sees a freshly generated report waiting for them.

Try adding a couple of jobs to your own application this week, and you will quickly see how much boilerplate the Quartz plugin removes compared to building a scheduler from scratch. The rest of the Grails Example library covers related topics such as packaging your application for deployment, securing scheduled endpoints, and wiring up continuous integration on popular Australian cloud platforms, so you can keep building from here.