skip to content

Explain the Trigger SPI in Spring scheduling, including CronTrigger, PeriodicTrigger, and TriggerContext.

level: middleimportance: must knowfreq 55%

answer

  1. nextExecution(TriggerContext) -> Instant or null, called after every run
  2. TriggerContext: lastScheduled / lastActual / lastCompletion + clock
  3. CronTrigger = cron expr; PeriodicTrigger = period
  4. PeriodicTrigger fixedRate vs fixedDelay
  5. first call: history is null

basics

~20 s

Trigger is an interface with nextExecution(TriggerContext) that returns the next run time. CronTrigger computes it from a cron expression; PeriodicTrigger from a fixed period. TriggerContext gives the last scheduled/actual execution and completion times so the next time can be derived.

solid answer

~50 s

The Trigger SPI decouples "when should this run next" from the task itself. Trigger has a single method — nextExecution(TriggerContext) returning an Instant (the next fire time) or null to stop. Spring calls it after each execution to schedule the following one, which is what makes schedules dynamic. TriggerContext exposes lastScheduledExecution (when the previous run was meant to start), lastActualExecution (when it really started), and lastCompletion (when it finished) — plus a Clock. Built-in implementations: CronTrigger wraps a cron expression and computes the next matching time from the last completion; PeriodicTrigger takes a period and can behave as fixed-rate (next = last scheduled + period) or fixed-delay (next = last completion + period), with an optional initial delay. Because nextExecution is a plain callback, you can implement Trigger yourself to read the interval from a database or config each cycle.

code

java · 17 lines
java
// A custom Trigger that reads its interval fresh each cycle from config/DB.
public class DynamicIntervalTrigger implements Trigger {

    private final IntervalSettings settings; // e.g. backed by a DB/config bean

    public DynamicIntervalTrigger(IntervalSettings settings) {
        this.settings = settings;
    }

    @Override
    public Instant nextExecution(TriggerContext ctx) {
        Instant last = ctx.lastCompletion();          // null on first run
        Instant base = (last != null) ? last : ctx.getClock().instant();
        long seconds = settings.getIntervalSeconds(); // re-read every cycle
        return base.plusSeconds(seconds);
    }
}

go deeper

for a junior

Know Trigger returns the next run time and CronTrigger/PeriodicTrigger are the built-ins.

for a middle

Explain nextExecution(TriggerContext), the three TriggerContext timestamps, and PeriodicTrigger's two modes.

for a senior

Discuss custom Trigger for DB-driven intervals, the null-first-call edge, and fixed-rate drift under a busy pool.

for a principal

Reason about DST/timezone in CronTrigger, coalescing of missed fixed-rate runs, and clock injection for testability.

## What the SPI is `org.springframework.scheduling.Trigger` is a **Service Provider Interface** — an extension point Spring's scheduler calls to ask *"given what just happened, when should this task run next?"* It has one method: ```java Instant nextExecution(TriggerContext triggerContext); ``` (Historically it returned `Date`; modern Spring 6 returns `java.time.Instant`.) Returning `null` tells the scheduler this task is done and should not be rescheduled. The key insight: **the scheduler invokes `nextExecution` after every run**. So the schedule is recomputed continuously rather than fixed once — this is the foundation of dynamic scheduling. ## TriggerContext `TriggerContext` carries the history the trigger needs to decide the next time. Its accessors (Spring 6, `Instant`-based): - `lastScheduledExecution()` — the time the previous execution was **scheduled** to start. - `lastActualExecution()` — the time it **actually** started (may lag if the pool was busy). - `lastCompletion()` — when the previous execution **finished**. - `getClock()` — the `Clock` to read "now" from (important for testability). On the very first call all three history values are `null`, so a trigger must handle the cold-start case (typically scheduling relative to `now` / the clock). ## CronTrigger `CronTrigger` is built from a cron expression (6-field Spring cron, seconds..day-of-week, plus macros like `@daily`). Its `nextExecution` computes the next timestamp matching the expression, based on the previous **completion** (or now on first run). It can take a time zone. It is the programmatic equivalent of `@Scheduled(cron=...)` but usable via `addTriggerTask`. ## PeriodicTrigger `PeriodicTrigger` is built from a period (`Duration`). Two modes: - **Fixed-rate** (`setFixedRate(true)`): next execution = `lastScheduledExecution + period` — runs are spaced by a constant interval regardless of how long each run takes. - **Fixed-delay** (default, `fixedRate=false`): next execution = `lastCompletion + period` — the gap starts only after the previous run finishes. It also supports an initial delay via `setInitialDelay(Duration)`. ## Why it matters — custom triggers Because `nextExecution` is called each cycle, you can implement `Trigger` to pull the interval from a database, feature flag, or config server on **every** invocation. Change the DB value and the very next scheduling decision picks it up — no restart, no re-registration. ## Gotchas - **First invocation:** all `TriggerContext` history is null — guard against NPE and schedule relative to the clock. - **Fixed-rate vs fixed-delay confusion:** with `PeriodicTrigger`, `fixedRate=true` uses `lastScheduledExecution`, not `lastActualExecution`; a task that overruns its period can cause back-to-back executions (missed-run coalescing behavior depends on the scheduler). - **Return `null` to stop** — a subtle way to make a self-terminating schedule, but easy to trigger accidentally and silently kill the task. - **Thread pool blocking:** if the single default scheduler thread is busy, `lastActualExecution` drifts from `lastScheduledExecution`; size the pool appropriately. - **Time zone / DST:** `CronTrigger` honors its configured zone; DST transitions can skip or duplicate a fire time.

  • What is the difference between lastScheduledExecution and lastActualExecution in TriggerContext?
    lastScheduledExecution is when the run was supposed to start; lastActualExecution is when it really started. They diverge when the scheduler thread pool is busy, so the task starts late. Fixed-rate uses the scheduled time; fixed-delay uses completion.
  • How does PeriodicTrigger distinguish fixed-rate from fixed-delay?
    With setFixedRate(true) the next time is lastScheduledExecution + period (constant cadence). Default (fixed-delay) is lastCompletion + period, so the gap begins only after the task finishes.
  • What does returning null from nextExecution do?
    It tells the scheduler the task is finished and should not be rescheduled — a way to build a self-terminating schedule, but a common accidental cause of a task silently stopping.

saying these in an interview costs you the question

  • Claiming Trigger.nextExecution is called only once at startup (it's called after every execution).
  • Confusing fixed-rate (uses lastScheduledExecution) with fixed-delay (uses lastCompletion).
  • Assuming TriggerContext history values are populated on the first invocation (they're null).

context