ADR-0018: Outbound-integration config is fail-fast at boot
ODD Platform validates an outbound integration's config when its bean is built — a missing value fails startup, not the first message; an unconfigured channel is simply off.
Status
Accepted. Reconstructed from the codebase on 2026-05-30; the decision is live in the source today.
Context
ODD Platform integrates with several outbound systems — Slack (data collaboration and notifications), email/SMTP, generic webhooks, LDAP. Each needs configuration the operator supplies: a token, a URL, a host. There are two moments such configuration can be validated: at boot, when the integration's bean is constructed, or lazily, when the first request tries to use it.
Lazy validation is the failure mode behind real incidents — a deployment looks healthy, then the first alert that should reach Slack silently never arrives because the token was blank. The operator discovers the misconfiguration in production, from its absence. ODD chose the opposite: surface a misconfigured integration at startup, as a deployment error, not as silent runtime degradation.
Decision
When an outbound integration is configured, the required values are checked as its bean is built, and an empty or invalid value throws — failing application startup. A blank Slack OAuth token, an empty webhook URL, a blank email sender/host/protocol all abort boot rather than producing a half-built client that fails later.
The check lives in the bean factory: the integration's @Bean method validates its inputs and throws IllegalArgumentException on an empty value before constructing the client. The same fail-at-boot discipline extends to the @ConfigurationProperties classes, whose @PostConstruct validators reject structurally invalid configuration (for example, a negative retry count or a missing LDAP URL) as the context starts.
The deliberate boundary — absence is not an error. This is not "every integration must be configured." Each notification channel's sender bean is gated by @ConditionalOnProperty on its own key (notifications.receivers.slack.url, …webhook.url, …email.sender). If an operator never sets a channel's key, the bean is never created — the channel is simply off, silently and by design. Fail-fast applies to an integration the operator opted into but configured incompletely; it does not force an operator to configure channels they don't want. The empty-value check inside each bean only runs once that bean is being built, i.e. once the key is present.
Consequences
A misconfigured-but-enabled integration is a startup failure with a named cause ("Slack OAuth token is empty"), not a silent runtime no-op. The operator learns at deploy time.
An operator enables exactly the channels they configure: setting a channel's key turns it on (and then its values must be valid); omitting the key leaves it off. There is no "enable notifications, then separately fill in each channel" two-step.
The trade-off of the absence-is-off rule: a typo in a channel's key name reads as "channel intentionally off," not as an error — the bean condition isn't met, so nothing is built and nothing complains. Fail-fast protects the values of a channel you turned on; it cannot protect the spelling of the key that turns it on.
The exception type signals where the fault is: an empty value in a bean factory throws
IllegalArgumentException(a bad argument to bean construction); a structurally invalid@ConfigurationPropertiesvalue throws from a@PostConstructvalidator (the deployment's configured state is wrong).
Evidence
odd-platform-api/.../datacollaboration/config/DataCollaborationConfiguration.java:23-24— theslackAPIClient()@Beanfactory:if (StringUtils.isEmpty(slackOauthToken)) { throw new IllegalArgumentException("Slack OAuth token is empty"); }before the Slack client is built.odd-platform-api/.../notification/config/NotificationConfiguration.java— the notification channel factories validate at construction:mailSenderthrows on blank sender / host / protocol (:40,:44,:48);slackNotificationSenderthrows on empty URL (:82);webhookNotificationSenderon empty URL (:95);emailNotificationSenderon blank recipient list (:111);alertNotificationMessageTranslatoron a negative downstream depth (:128) — allIllegalArgumentException.odd-platform-api/.../notification/config/NotificationConfiguration.java:37,75,89,102— the absence-is-off boundary: each sender bean carries@ConditionalOnProperty(name = "notifications.receivers.{slack.url|webhook.url|email.sender}"), so an unset key means the bean is never created and the channel is silently off; the empty-value check only fires once the key is present.odd-platform-api/.../auth/ODDLDAPProperties.javaand.../datacollaboration/config/DataCollaborationProperties.java—@ConfigurationPropertiesclasses whose@PostConstructvalidators throwIllegalStateExceptionon structurally invalid configuration (a missing LDAP server URL; a negative message-retry count), applying the same fail-at-boot discipline from the properties-binding side.
See also
Data Collaboration — the Slack integration whose OAuth token is validated at boot.
Notifications — the per-channel configuration whose presence enables a channel and whose values are checked when it is built.
LDAP — an authentication integration whose properties are validated as the context starts.
Last updated