CI / lint (pull_request) Successful in 4m27s
CI / build (pull_request) Successful in 1m11s
CI / unit (pull_request) Successful in 1m17s
CI / frontend (pull_request) Successful in 2m53s
CI / mutation (pull_request) Successful in 5m59s
CI / verify-stack (pull_request) Successful in 8m52s
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
80 lines
3.5 KiB
Markdown
80 lines
3.5 KiB
Markdown
# 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 Quartz `IJob`) is fired by a cron trigger — daily
|
|
at 03:00 by default, overridable with `Quartz__Cron`. It is a thin shell: it
|
|
resolves the pure `HerregistratieReminderSweep` (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:
|
|
|
|
1. **A `BackgroundService` with a 24h `Task.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.
|
|
2. **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.
|