Reactor Catalogue
1. Overview
This page is the reactor-specific companion to Signal & Sweep Framework Architecture. The architecture page describes the machinery; this page records what we actually run on it — which signal types exist, what each aggregate key means, and which reactors consume what.
Read the architecture page first. Nothing here makes sense without the concepts of signal, reactor, sweep and squash.
2. Delivery Status
The framework is being delivered in phases under Feature #403. Most of the catalogue below is planned, not built. Check this table before assuming a reactor exists.
| Reactor | Status | Notes |
|---|---|---|
|
Phase 1 |
Framework-internal. Purges expired signals; validates the sweep path with no domain risk. |
|
Phase 3 |
First functional target. Order-level email on registration and payment. |
|
Deferred |
Migration blocked — see LeaderboardReactor — deferred. |
All others |
Not started |
Each arrives with its parent design thread. |
3. Signal Types
| Signal type | Emitted when | Consumed by | Debounce |
|---|---|---|---|
|
Race results added, modified or deleted |
LeaderboardReactor |
60s |
|
Registration process completes and a draft Order is created |
CommunicationReactor, SeedingReactor |
0s |
|
Order transitions to PAID |
CommunicationReactor |
0s |
|
Payment gateway reports failure |
CommunicationReactor |
0s |
|
A person merge completes |
LeaderboardReactor, NumberTagReactor |
60s |
|
|
LeaderboardReactor, SeedingReactor |
15m |
|
Membership transitions to ACTIVE |
CommunicationReactor |
0s |
|
Any order status transition |
CommunicationReactor |
0s |
Communication signals use a zero debounce deliberately. Each occurrence corresponds to one message to one customer; squashing two of them would silently drop a communication. Recalculation signals use a non-zero window because the work is expensive and the result only depends on final state.
3.1. Why there is no ENTITY_MODIFIED
A generic "some entity changed" signal was designed and then removed. A bulk import of 500 participants would emit 500 signals; even squashed by entity type, the table becomes noise, and the remote APIs that would consume it are better served by batched updates. Change detection for sync already exists as timestamp comparison, which is a sweep by another name. See DataSyncReactor — sweep-only.
If a future integration genuinely needs near-real-time push, add a target-specific signal type rather than resurrecting a generic one.
4. Aggregate Key Conventions
The aggregate key defines what "the same work" means for squashing. Get it wrong and you either lose signals or fail to coalesce them.
| Pattern | Meaning |
|---|---|
|
Work is per series. All result changes anywhere in the series collapse into one recalculation. |
|
Work is per order. Used for registration and payment communications. |
|
Work is per event participant. |
|
Work is per event — used where a reactor processes a whole event’s participants in one pass. |
Two rules:
-
Key at the granularity the reactor actually works at. If the reactor recalculates a whole series, keying by participant produces N redundant recalculations of the same series.
-
The key must be stable for the duration of the debounce window. Keys derived from mutable state can split what should have been one unit of work.
A signal type consumed by two reactors that work at different granularities is a design smell. Either split the signal type, or key at the coarser granularity and let the finer reactor narrow it down.
5. Reactor Catalogue
| Reactor | Signal types | Debounce key | Sweep | Style |
|---|---|---|---|---|
|
(none) |
— |
Nightly |
Direct |
|
|
|
Every 10 min |
Direct |
|
|
|
Every 15 min (reminders) |
Direct, then Fluxnova |
|
(none — sweep-only) |
— |
Every 5 min |
Direct |
|
(none — sweep-only) |
— |
Daily 03:00 |
Direct |
|
|
|
Daily 02:30 |
Direct |
|
|
|
Within DataSync sweep |
Direct |
|
(none — sweep-only) |
— |
5 min dev / 15 min prod |
Direct |
|
(none — sweep-only) |
— |
Daily 02:00 |
Direct |
|
(none — sweep-only) |
— |
Weekly Sun 04:00 |
Direct |
Sweep ordering is expressed through cron times rather than an explicit dependency graph. IdentityRefreshReactor at 02:00 precedes SeedingReactor at 02:30 because seeding depends on refreshed identity data. This is sufficient while the catalogue is small; a real dependency graph would be premature.
6. Reactor Notes
6.1. CommunicationReactor
Handles registration and payment communications. Two decisions shape it:
Order-level, not participant-level. One email per order, addressed to the billing email. An order with seventeen participants sends one message, not seventeen to the same inbox. Order and participant emails carry genuinely different content — an order email is buyer-facing (reference, total, payment status, one pay link, a summary of who is entered), a participant email is athlete-facing (their category, race number, start group, event-day logistics). They are separate templates, not one template with a different recipient. Per-participant email is a later addition.
Direct, not via Fluxnova — for now. The framework routes single-step reactions in-process and reserves Fluxnova for multi-step orchestration. Order confirmation is single-step, and Fluxnova remains deployed but unconfigured, so the reactor calls the email service directly. Multi-step escalation — a payment reminder sequence with channel fallback — remains the Fluxnova case and will be added as a separate path rather than by rewriting this one.
Idempotency is a hard prerequisite. A reactor that sends email will be retried by the framework after a transient SMTP failure. Without a dedup guard in communication_log, that retry sends a second real email to a real customer. The guard must be in place before this reactor is enabled anywhere. See Email Service.
6.2. DataSyncReactor — sweep-only
The bi-directional sync framework already detects change by comparing lastModifiedMain and lastModifiedRemote timestamps. That is a sweep. Adding reactive signals on top would duplicate the detection, flood the signal table during bulk imports, and turn efficient batch API calls into chatty per-entity ones.
The sweep runs every five minutes, collects everything changed since the last watermark, and submits it in one batch. See Data Synchronisation.
6.3. NumberReassignmentReactor
Closes the loop on the DETACH-and-reconcile cascade introduced by Feature #473. When an admin reassigns a race number, the system nulls number_id on the same participant’s future same-type event participants. Those sit unassigned until something reconciles them — this reactor.
It selects event participants with a null number on future events whose event type requires one, then calls the pool service per participant in its own transaction, so one failure does not block the rest. Assignment obeys ADR-0006: prefer the participant’s latest used number of that subtype, else draw from stock; if their latest is UNFIT_FOR_SERVICE, leave it unassigned and surface it as a validation error rather than auto-assigning.
Until this reactor exists, reconciliation is manual via the event-finalisation gate.
6.4. LeaderboardReactor — deferred
The existing LeaderboardSynchronizationService was the intended first migration: it already implements the signal-plus-poll pattern, using an in-memory map that does not survive restarts. Moving it onto the framework would give it durability, retry and visibility.
That migration is deferred. Its core operation — upserting calculated entries into a ResultSet — is still an unimplemented TODO, so the service currently computes leaderboard entries and discards them. Migrating it would validate nothing about the framework and would move a half-finished feature onto new foundations.
Until then, ResultSetChangedEvent and LeaderboardSynchronizationService stay exactly as they are. Where RESULT_SET_CHANGED signals are emitted, they are emitted alongside the existing Spring ApplicationEvent, not instead of it, so leaderboard behaviour is unchanged.
7. Multi-Reactor Failure
When two reactors consume the same signal and one fails, per-reactor delivery tracking means only the failure is retried.
The lesson for reactor authors: your reactor’s failure is yours alone. Do not throw to signal a problem in someone else’s concern, and do not assume a retry means you failed.
8. Related Documentation
-
Signal & Sweep Framework Architecture — the framework itself
-
Adding a Reactor — implementation cookbook
-
Leaderboard Synchronization — the deferred migration target
-
Email Service — CommunicationReactor’s downstream
-
Data Synchronisation — DataSyncReactor’s downstream
-
RaceNumber Lifecycle — context for NumberReassignmentReactor