ADR-0005 — Capability-flagged storage abstraction, MinIO/S3 first
- Status: Accepted
- Date: 2026-06-11
- Related: 01 R3, 05 Storage, ADR-0011
Context
The brief requires multi-cloud storage across MinIO, S3, R2, GCS, and Azure Blob. These differ in consistency, multipart semantics, presigned-URL capabilities, conditional writes, and error taxonomies (R3). Two failure modes loom: an abstraction that leaks provider quirks, or one that collapses to a useless lowest-common-denominator. Building all five adapters before shipping to one customer is also classic overengineering (Ledger).
Decision
Define a narrow, capability-flagged storage interface in the Storage context:
- Core operations:
Put,Get,Head,Delete,List,Presign(method, key, constraints, ttl), and a multipart group (InitMultipart,PresignPart,CompleteMultipart,AbortMultipart). - Capability flags advertise per-provider support (e.g.
SupportsConditionalPut,PresignSupportsContentLengthRange,MaxPartSize). Callers query capabilities; the abstraction never silently emulates a missing feature. - One conformance test suite every adapter must pass — the contract is the tests, not just the interface.
- Ship exactly one adapter first: MinIO (S3-compatible). S3 follows trivially. R2/GCS/Azure are added in P5 (09), each gated by passing conformance.
- Object keys are content-addressed and tenant-prefixed:
/{tenant_id}/{sha256}.
Consequences
Positive
- The abstraction (the portfolio deliverable) exists from day one without the cost of five adapters.
- Capability flags prevent the leaky/lowest-common-denominator trap (R3).
- Conformance suite makes adding a provider a known, bounded task.
- Content-addressed keys enable dedup (I2) and clean GC; tenant prefix enables isolation and per-tenant lifecycle (ADR-0007).
Negative / costs
- Capability flags push some branching to callers (e.g. fall back when conditional writes are unavailable) — acceptable and explicit, vs. hidden emulation.
- MinIO-first means S3-specific behaviors must be validated when S3 is enabled (conformance suite covers this).
- Provider differences in strong-read-after-write must be respected; we never
read bytes back to confirm a write — we
Headfor size/etag (commit protocol).
Alternatives considered
- Adopt a library that wraps all providers (e.g. a
gocloud-style blob package): considered as the implementation behind our interface; the decision here is the interface + capability model + conformance, which we own regardless of the underlying lib. We keep the seam ours so provider quirks never leak upstream. - Target one provider only (no abstraction): rejected — multi-cloud pluggability is a stated product bet (G4) and portfolio goal.
- Build all five adapters up front: rejected — overengineering; no demonstrated demand; conformance suite makes later addition cheap.