> For the complete documentation index, see [llms.txt](https://docs.opendatadiscovery.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0018-fail-fast-outbound-config-at-boot.md).

# ADR-0018: Outbound-integration config is fail-fast at boot

## 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&#x20;*****is*****&#x20;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 `@ConfigurationProperties` value throws from a `@PostConstruct` validator (the deployment's configured state is wrong).

## Evidence

* `odd-platform-api/.../datacollaboration/config/DataCollaborationConfiguration.java:23-24` — the `slackAPIClient()` `@Bean` factory: `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: `mailSender` throws on blank sender / host / protocol (`:40`, `:44`, `:48`); `slackNotificationSender` throws on empty URL (`:82`); `webhookNotificationSender` on empty URL (`:95`); `emailNotificationSender` on blank recipient list (`:111`); `alertNotificationMessageTranslator` on a negative downstream depth (`:128`) — all `IllegalArgumentException`.
* `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.java` and `.../datacollaboration/config/DataCollaborationProperties.java` — `@ConfigurationProperties` classes whose `@PostConstruct` validators throw `IllegalStateException` on 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](/features/active-platform-features/data-collaboration.md) — the Slack integration whose OAuth token is validated at boot.
* [Notifications](/features/active-platform-features/notifications.md) — the per-channel configuration whose presence enables a channel and whose values are checked when it is built.
* [LDAP](/configuration-and-deployment/enable-security/authentication/ldap.md) — an authentication integration whose properties are validated as the context starts.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.opendatadiscovery.org/developer-guides/architecture-decision-log/adr-0018-fail-fast-outbound-config-at-boot.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
