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: true activates 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:173enabled: false under the notifications: key (line 172), the verbatim shipped default.

  • odd-platform-api/.../notification/config/NotificationsFeatureCondition.java:11-13 — reads FeatureResolver.NOTIFICATIONS_ENABLED_PROPERTY from the Environment with default false.

  • 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

Last updated