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/FilePathConstructorinterfaces).📌 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 (useREMOTEfor any deployment where users actually upload files, plus the in-flight chunk-staging and AWS S3 region caveats). This ADR records why the boot-timeLOCAL-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:26and.../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:36and.../config/MinioConfig.java:10—@ConditionalOnProperty(value = "attachment.storage", havingValue = "REMOTE"); the MinIO/S3 backend.odd-platform-api/src/main/resources/application.yml:216—storage: LOCAL(the explicit shipped default);:219—local.path: /tmp/odd/attachments(the container-local default path).odd-platform-api/.../service/attachment/local/LocalFilePathConstructor.java:15-16— the LOCAL path is bound fromattachment.local.path, confirming LOCAL writes to a configurable container path.
See also
Attachments — the user-facing feature and the storage-mode caveats.
Attachment Storage Configuration — the operator reference for
LOCALvsREMOTE, persistence, and the AWS S3 region constraint.
Last updated