ADR-0022 records using Quartz.NET for time-triggered fleet sweeps (pumps stay as queue-drainers); demo-script and BACKLOG describe S-17's outcome. refs #18
3.5 KiB
ADR-0022: Quartz.NET for time-triggered fleet sweeps
- Status: Accepted
- Date: 2026-07-23
- Deciders: Respellion engineering
- Slice: S-17 (#18) · Proposal issue: #120
Context
A BIG inscription is valid for a fixed term; before it lapses the zorgprofessional must herregistreren. S-17 adds a herregistratie reminder sweep: once a day, scan the register for inscriptions whose deadline is within the reminder window and remind each one.
The Domain Service already runs periodic background work — OpenZaakJobPump,
BeoordelingEscalatiePump, RegistratieVerlopenPump. Those are continuous job
pollers: they drain Flowable's external-task/job queues at-least-once, picking up
work as soon as it is parked, on a short poll interval. The reminder sweep is a
different shape of work: time-triggered, once a day, over our own store — there
is no queue to drain and no "as soon as possible" requirement.
The PRD already names the scheduler component: "Scheduler (Quartz.NET): fleet-wide sweeps (expiry, reminders)" (§39, §94). Adding Quartz.NET is nonetheless a new dependency, so this decision is recorded before the code lands (CLAUDE.md §14).
Decision
Use Quartz.NET for time-triggered fleet sweeps, starting with the herregistratie
reminder sweep. Leave the existing pumps as BackgroundService job pollers.
HerregistratieReminderJob(a QuartzIJob) is fired by a cron trigger — daily at 03:00 by default, overridable withQuartz__Cron. It is a thin shell: it resolves the pureHerregistratieReminderSweep(application layer) and logs how many reminders went out.- The sweep's rule lives in the domain:
Registration.HerregistratieReminderDue(asOf), which the store query and the sweep both build on. The sweep marks each reminded inscription (HerregistratieReminderVerstuurd), so a re-fire reminds no one twice (§8.6).
Two options were rejected:
- A
BackgroundServicewith a 24hTask.Delay. No new dependency, but it drifts to process-start time, has no cron/misfire semantics, and contradicts the PRD's named component. A daily "run at 03:00" is exactly what cron scheduling is for. - Migrating the three pumps onto Quartz too, for one mechanism. Rejected: the pumps are not schedulers. Forcing a "run at time T" tool onto "drain this queue continuously" work is churn and a boundary change for negative benefit. The teachable distinction is worth keeping: pumps drain queues; Quartz fires sweeps.
Consequences
Positive
- Cron scheduling with restart-stable timing and misfire handling, for free.
- The reminder rule is one domain method, reused by the store query and the sweep; the scheduler owns none of the policy.
- The reference app now demonstrates the intended Scheduler component.
Negative / costs
- One new dependency (
Quartz,Quartz.Extensions.Hosting) in the Domain Service. - Two periodic-work mechanisms coexist (pumps + Quartz). Deliberate — they model two genuinely different concerns, documented here.
Follow-up
- The validity term (5 years) and reminder lead time (16 weeks) are domain calibration knobs; promote them to beheer config (S-15) if a demo needs them per-catalogus.
- The Quartz job stores its schedule in RAM (
RAMJobStore); a persistent/clustered store is a later concern if the Domain Service is scaled out.
Coupling rules touched (CLAUDE.md §8)
None. Quartz is internal to the Domain Service and drives an application use case over the store port. No ZGW or Flowable coupling is added; the sweep talks to no peer module.