skip to content

How would you implement a DB-driven schedule where an admin can change a job's interval without restarting the app?

level: seniorimportance: must knowfreq 50%

answer

  1. addTriggerTask(runnable, custom Trigger)
  2. nextExecution reads DB every cycle -> live changes
  3. delegate to new CronTrigger(currentCron)
  4. cache DB read, validate cron
  5. not cluster-aware -> ShedLock / leader election

basics

~20 s

Register the task with ScheduledTaskRegistrar.addTriggerTask, passing a custom Trigger whose nextExecution reads the current interval from the database each time it's called. Since Spring calls nextExecution after every run, changing the DB value changes the next scheduled time.

solid answer

~50 s

In a SchedulingConfigurer.configureTasks, register the job via addTriggerTask(runnable, trigger). The trigger is a custom Trigger implementation whose nextExecution(TriggerContext) queries the current interval (or cron) from the database on every call — Spring invokes it after each execution to compute the next fire time, so any DB change is picked up on the following cycle with no restart. For a cron string you can build a fresh CronTrigger per call and delegate to it; for a period you compute lastCompletion + Duration. Give the registrar a real TaskScheduler pool so the DB read and the job don't contend on a single thread. Keep the DB read cheap (cache with short TTL) since it runs every cycle. If an admin can also enable/disable the job, return null to stop it, or cancel its ScheduledFuture and re-register when re-enabled.

code

java · 28 lines
java
@Configuration
@EnableScheduling
class DynamicJobConfig implements SchedulingConfigurer {

    private final ScheduleRepository scheduleRepo;
    private final ReportJob reportJob;

    DynamicJobConfig(ScheduleRepository r, ReportJob j) {
        this.scheduleRepo = r; this.reportJob = j;
    }

    @Bean(destroyMethod = "shutdown")
    ScheduledExecutorService jobPool() {
        return Executors.newScheduledThreadPool(4);
    }

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        registrar.setScheduler(jobPool());
        registrar.addTriggerTask(
            reportJob::run,
            ctx -> {
                String cron = scheduleRepo.currentCron();   // fresh each cycle
                if (cron == null) return null;               // disabled -> stop
                return new CronTrigger(cron).nextExecution(ctx);
            });
    }
}

go deeper

for a junior

Recognize that a custom Trigger with addTriggerTask is the tool; details optional.

for a middle

Implement the trigger delegating to a fresh CronTrigger and know the change applies next cycle.

for a senior

Handle enable/disable, caching the DB read, validation, and the single-thread pool pitfall.

for a principal

Address clustering (ShedLock/leader election), immediate-apply via ScheduledFuture cancel/re-register, and when to switch to Quartz.

## The pattern The requirement — change a schedule at runtime without a redeploy — is the canonical use case for the `Trigger` SPI plus `ScheduledTaskRegistrar.addTriggerTask`. ### Step 1 — implement SchedulingConfigurer ```java @Configuration @EnableScheduling class DynamicJobConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar r) { r.setScheduler(taskScheduler()); // real pool r.addTriggerTask(this::runJob, dbDrivenTrigger()); } } ``` ### Step 2 — the Trigger reads the DB each cycle Because Spring calls `nextExecution` **after every execution**, the trigger is your live hook into current configuration: ```java Trigger dbDrivenTrigger() { return ctx -> { String cron = scheduleRepo.currentCron(); // fresh read return new CronTrigger(cron).nextExecution(ctx); }; } ``` Change the row, and the *next* scheduling decision uses the new value. No restart, no bean re-creation. ## Cron vs period - **Cron string in DB:** build a `CronTrigger` from the current string and delegate to it (as above). Delegating reuses Spring's parsing/timezone logic and handles the null-history first call. - **Interval in DB:** compute `base.plus(Duration.ofSeconds(n))` where `base = ctx.lastCompletion()` (fixed-delay) or `ctx.lastScheduledExecution()` (fixed-rate), falling back to `ctx.getClock().instant()` on first run. ## Enable / disable at runtime - Return `null` from `nextExecution` when the job is disabled — the scheduler stops rescheduling it. To restart, you must **re-register** the task (returning non-null again isn't enough because it's no longer polled once stopped). - Cleaner for on/off: keep the `ScheduledFuture` returned when registering (via `TaskScheduler.schedule(...)` directly, or by tracking `ScheduledTask` from the registrar), and `cancel(false)` it on disable, re-schedule on enable. ## Performance & correctness gotchas - **The DB read runs every cycle.** For frequent jobs this hammers the DB. Cache the schedule value with a short TTL, or invalidate the cache when the admin saves. - **Invalid cron from admin input:** a bad string throws inside `nextExecution`; guard/validate and fall back to a safe default, or the task dies silently. - **Single-thread default scheduler:** without `setScheduler`, one slow job blocks all others and blocks the trigger recomputation. Always supply a `ThreadPoolTaskScheduler`. - **Multi-instance / clustering:** every app instance schedules independently, so N instances run the job N times. Spring's scheduler is **not** cluster-aware — add a distributed lock (ShedLock) or leader election if the job must run once globally. - **First run:** handle null `TriggerContext` history. - **Exceptions in the Runnable:** an uncaught exception is logged but Spring still reschedules based on the trigger; make the job idempotent and defensive. ## When to reach for a real scheduler instead If you need persistence of schedules, misfire handling, clustering, and history out of the box, Quartz is the heavier alternative. The `Trigger`/`addTriggerTask` approach is the lightweight, in-process choice when you own persistence and don't need cluster coordination.

  • The admin changes the cron in the DB. When does the job actually pick up the new schedule?
    On the next call to nextExecution, i.e. right after the current/next execution completes. The already-scheduled run fires on the old cadence; the one after it uses the new value. If you need it to apply immediately, cancel the outstanding ScheduledFuture and re-register.
  • You run three instances behind a load balancer. What happens to this job and how do you fix it?
    Each instance schedules and runs it independently, so it executes three times per cycle. Spring scheduling isn't cluster-aware — add a distributed lock like ShedLock, use leader election, or move to Quartz with a JDBC job store.
  • What's the risk of reading the schedule from the DB inside nextExecution for a job that runs every second?
    A DB round-trip every cycle adds load and latency and can block the scheduler thread. Cache the value with a short TTL or invalidate on admin update, and use a dedicated pool so the read doesn't starve other tasks.

saying these in an interview costs you the question

  • Thinking you must restart the app or re-create the bean to change the schedule.
  • Reading the DB inside configureTasks instead of inside nextExecution (that reads only once at startup).
  • Ignoring that every clustered instance runs the job independently.
  • Not validating admin-supplied cron, letting a bad expression silently kill the task.

context