ADR-0041: Notification channels activate by the presence of their keys

Each notification channel — Slack, webhook, email — turns on by the presence of its own config key; an unset key means no bean and no delivery, with no separate per-channel enable flag.

Status

Accepted. Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.

Context

Once the Notifications subsystem is enabled (ADR-0040), an operator still has to choose where alerts go: Slack, a generic webhook, email, or some combination. The platform needs a way to activate exactly the channels an operator wants — without a separate enable flag per channel that could drift out of step with whether the channel is actually configured.

Decision

Each channel activates by the presence of its own configuration key. The three sender beans each carry @ConditionalOnProperty on the key that channel needs — notifications.receivers.slack.url, notifications.receivers.webhook.url, notifications.receivers.email.sender. Setting the key creates the bean (the channel is on); leaving it unset means no bean is created and that channel is silently absent. There is no separate slack.enabled / email.enabled flag — the configuration key is the toggle.

This composes with ADR-0040 into a two-stage opt-in: the subsystem must be enabled (notifications.enabled: true), and then each channel is activated by populating its key. An operator who enables notifications but sets no channel keys gets a running subsystem that delivers nowhere.

The presence test is for the key being set; a key set to an empty value is caught separately by the fail-fast empty-value check inside the sender's bean factory (ADR-0018), which aborts startup rather than building a broken sender.

Consequences

  • Operators configure only the channels they want, by populating only those keys — the common case is "set one key, get one channel."

  • Adding a new channel is an add-a-bean-method change carrying its own @ConditionalOnProperty, not a change to a central channel registry.

  • The trade-off is implicitness: an operator who omits a channel's key gets no warning (it is indistinguishable from deliberately not wanting that channel). A malformed (empty) key is the case the fail-fast check catches; an absent key is by-design silent.

  • Channel activation is independent — one configured channel works regardless of whether the others are set.

Evidence

  • odd-platform-api/.../notification/config/NotificationConfiguration.java:37@ConditionalOnProperty(name = "notifications.receivers.email.sender") on the mailSender bean; :102 — the same key on the emailNotificationSender bean (the email channel needs two beans, both presence-gated on one key).

  • odd-platform-api/.../notification/config/NotificationConfiguration.java:75@ConditionalOnProperty(name = "notifications.receivers.slack.url") on slackNotificationSender; :89@ConditionalOnProperty(name = "notifications.receivers.webhook.url") on webhookNotificationSender.

  • odd-platform-api/src/main/resources/application.yml:180-186 — the notifications.receivers.{slack.url|webhook.url|email.sender} keys ship empty, so no channel is active by default.

See also

Last updated