ADR-0040: Notifications ship disabled by default behind one condition
ODD Platform ships notifications off by default — one Condition reads notifications.enabled and a single meta-annotation gates every component, so the whole subsystem turns on from one switch.
Status
Accepted. Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.
Context
The Notifications subsystem is heavyweight: it consumes the Postgres write-ahead log via a replication slot, runs a background subscriber, and fans alerts out to external channels. It spans several Spring components (a bean-wiring @Configuration, a startup subscriber, a message processor). Most deployments don't need it. The platform needs it off by default, turned on by a single switch, with no risk that one component activates while another stays dormant.
Decision
The Notifications subsystem ships notifications.enabled: false and is gated by a single Condition class consulted through one @ConditionalOnNotifications meta-annotation applied to every Notifications component. The condition (NotificationsFeatureCondition) reads notifications.enabled from Spring's Environment, defaulting to false. The reusable @ConditionalOnNotifications annotation wraps @Conditional(NotificationsFeatureCondition.class) and is placed on all three top-level components — the @Configuration that wires the senders, the startup subscriber, and the alert message processor. When the property is not true, none of the three register and the WAL subscriber never starts.
The design is single-source-of-truth gating: one property, one condition class, one meta-annotation. A developer changing the gating semantics (say, sourcing the flag from a feature-flag service) edits one file rather than each component's annotation. This is the meta-annotation variant of the platform's ship-disabled-by-default family — the same posture as GenAI (ADR-0004, inline) and Data Collaboration (ADR-0019, which uses the identical meta-annotation idiom).
Consequences
A default deployment runs with notifications entirely absent — no subscriber, no WAL replication slot created, no senders wired.
The subsystem turns on as a unit: setting
notifications.enabled: trueactivates all three components together, so there is no partially-enabled state where (for example) the processor exists but the subscriber doesn't.Enabling the subsystem is necessary but not sufficient to receive notifications — individual channels then activate by the presence of their own keys (ADR-0041). The two together form a two-stage opt-in.
Centralising the flag in one condition class avoids the drift that scattered per-component property checks would cause; the trade-off is one extra indirection (a meta-annotation) over a bare
@ConditionalOnProperty, chosen deliberately because the flag has three consumers.
Evidence
odd-platform-api/src/main/resources/application.yml:173—enabled: falseunder thenotifications:key (line 172), the verbatim shipped default.odd-platform-api/.../notification/config/NotificationsFeatureCondition.java:11-13— readsFeatureResolver.NOTIFICATIONS_ENABLED_PROPERTYfrom theEnvironmentwith defaultfalse.odd-platform-api/.../notification/config/ConditionalOnNotifications.java:9-12— the meta-annotation:@Conditional(NotificationsFeatureCondition.class).The annotation is applied to all three components:
NotificationConfiguration.java:27(@Configuration),NotificationSubscriberStarter.java:17(startup subscriber),AlertNotificationMessageProcessor.java:15(alert processor).
See also
ADR-0041 — Notification channels activate by the presence of their keys — the second stage of the opt-in, once the subsystem is enabled.
ADR-0004 — GenAI ships disabled by default and ADR-0019 — Data Collaboration ships disabled by default — the same ship-off-by-default posture.
Last updated