ADR-0012: Attachment storage backend is selected at boot

ODD Platform picks its attachment storage backend at boot from attachment.storage — LOCAL is the implicit default, REMOTE is S3/MinIO, and switching backends needs a restart.

Status

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

Context

Data-entity attachments (uploaded files and remote-URL links) need somewhere to live. ODD Platform supports two backends: a LOCAL filesystem path inside the platform container, and a REMOTE S3-compatible object store (MinIO or AWS S3). The platform needs a way to choose between them, and a default for operators who set nothing.

The choice is which moment the backend is fixed: as a runtime switch the platform could flip per request, or as a boot-time decision baked into the bean graph. ODD chose boot-time wiring — the backend is decided once, when the application context starts.

Decision

The attachment storage backend is selected at boot via @ConditionalOnProperty on the attachment.storage property. Each backend's beans are conditionally created: the LOCAL implementations carry @ConditionalOnProperty(value = "attachment.storage", havingValue = "LOCAL", matchIfMissing = true), and the REMOTE (MinIO) implementations carry havingValue = "REMOTE". Exactly one backend's beans are instantiated for the lifetime of the process.

LOCAL is the implicit default. The matchIfMissing = true on the LOCAL beans means a deployment that never sets attachment.storage runs LOCAL. The shipped application.yml also states storage: LOCAL explicitly, so the default is visible to an operator reading the config, not only implied by the code.

Because the selection is a @ConditionalOnProperty condition evaluated at context startup, switching backends requires a Platform restart — there is no runtime toggle.

Consequences

  • An operator chooses the backend with one property and a restart; the rest of the attachment code is backend-agnostic (both implement the same FileUploadService / FilePathConstructor interfaces).

  • 📌 The default backend is durability-limited, and the operator docs carry the caveat. Under LOCAL, files are written to a container-local path (attachment.local.path, default /tmp/odd/attachments). On a containerised deployment that path is wiped on any container or pod restart, so a default deployment loses uploaded attachments on restart. This is a consequence operators must plan for — the Attachments page and the Attachment Storage Configuration operator reference carry the full guidance (use REMOTE for any deployment where users actually upload files, plus the in-flight chunk-staging and AWS S3 region caveats). This ADR records why the boot-time LOCAL-default selection exists; those pages tell an operator what to do about it.

  • Adding a third backend means adding a new @ConditionalOnProperty(havingValue = "…") implementation set, not a runtime branch — the selection mechanism scales by adding conditioned bean sets.

Evidence

  • odd-platform-api/.../service/attachment/local/LocalFileUploadServiceImpl.java:26 and .../local/LocalFilePathConstructor.java:13@ConditionalOnProperty(value = "attachment.storage", havingValue = "LOCAL", matchIfMissing = true); LOCAL is the implicit default.

  • odd-platform-api/.../service/attachment/remote/RemoteFileUploadServiceImpl.java:36 and .../config/MinioConfig.java:10@ConditionalOnProperty(value = "attachment.storage", havingValue = "REMOTE"); the MinIO/S3 backend.

  • odd-platform-api/src/main/resources/application.yml:216storage: LOCAL (the explicit shipped default); :219local.path: /tmp/odd/attachments (the container-local default path).

  • odd-platform-api/.../service/attachment/local/LocalFilePathConstructor.java:15-16 — the LOCAL path is bound from attachment.local.path, confirming LOCAL writes to a configurable container path.

See also

Last updated