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 themailSenderbean;:102— the same key on theemailNotificationSenderbean (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")onslackNotificationSender;:89—@ConditionalOnProperty(name = "notifications.receivers.webhook.url")onwebhookNotificationSender.odd-platform-api/src/main/resources/application.yml:180-186— thenotifications.receivers.{slack.url|webhook.url|email.sender}keys ship empty, so no channel is active by default.
See also
ADR-0040 — Notifications ship disabled by default behind one condition — the subsystem gate; this record is the per-channel second stage.
ADR-0018 — Outbound-integration config is fail-fast at boot — the empty-value check that turns a malformed channel key into a startup failure.
Last updated