Files
register-referentie/docs/architecture/adr-0022-quartz-scheduler.md
T
not ccb268bdd5
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
docs: ADR-0022 + demo note + backlog sync for the herregistratie sweep (refs #18)
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
2026-07-23 12:01:36 +02:00

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 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.