DI topology and injected contracts¶
Status: accepted Date: 2026-08-20
Context¶
The backend is a single process. Many objects are constructed before the DI
container, registered as singletons, or accessed through static facades.
Issue #247 covers
IDbContextFactory for hosted services. This ADR records the rest of the
topology and the first five contracts. DI does not by itself enable horizontal
scaling: blobs, metrics, Warden/cache files, and worker ownership stay
process-local.
PostgreSQL is already supported for the main database. The historical SQLite-only claim on #857 is stale.
Current topology¶
flowchart TB
subgraph bootstrap [Pre-container bootstrap]
Config[ConfigManager LoadConfig]
Blob[FileBlobStore]
Rclone[RcloneClient.Initialize]
WS[new WebsocketManager]
Logs[LogBufferSink / StreamTrace]
end
subgraph container [DI container]
Scoped[Scoped DavDatabaseClient]
Singletons[Hosted-service singletons]
Aliases[Interface aliases]
end
subgraph statics [Process-global assignments]
Prom[PrometheusMetrics.Current]
Budget[InFlightArticleBudget.Current]
BlobFacade[BlobStore.Use]
RcloneCurrent[RcloneClient.Current]
end
Config --> container
Blob --> BlobFacade
Rclone --> RcloneCurrent
WS --> Aliases
Singletons --> statics
| Kind | Examples | Classification |
|---|---|---|
| Immutable process configuration | env overlay, thread-pool limits | Keep as bootstrap |
| Mutable persisted state | ConfigManager |
IConfigReader / IConfigUpdater / IConfigChangeSource |
| Resource owner | QueueManager, UsenetStreamingClient, FileBlobStore |
Inject; queue workers stay internal |
| Cache | blob metadata cache, segment cache | Owned by the resource singleton |
| Telemetry facade | PrometheusMetrics.Current, stream trace |
Separate later slice |
| Test seam only | RcloneClient.TestHandler, queue context overrides |
Keep explicit |
Decision¶
Extract contracts for the five highest-coupling seams without duplicating #247:
- Config — split read / update / change-source. Typed getters remain on
ConfigManageruntil grouped by domain. Subscribers dispose viaIConfigChangeSource.Subscribe. - Blob storage —
IBlobStore/FileBlobStore. StaticBlobStoreforwards to the DI singleton afterBlobStore.Use. - rclone —
IRcloneClient.RcloneClientis an instance that unsubscribes on dispose.RcloneClient.Currentis the remaining static bridge forDavDatabaseContext.RcloneVfsForget. - Queue —
IQueueCoordinatorfor controller/health operations.QueueManagerkeeps worker ownership. - WebSocket —
IWebsocketPublisherfor publish-only consumers. Session management stays onWebsocketManager.
Do not extract interfaces solely to raise interface count. Process-global
telemetry (PrometheusMetrics.Current, in-flight budget, repair sinks)
stays a later slice.
Consequences¶
Call sites can take narrow contracts. Remaining static bridges are documented
and must not gain new consumers. Completing #247 and migrating leftover
new DavDatabaseContext() helpers is out of this ADR. Two independent test
hosts can still share RcloneClient.Current / BlobStore until those
bridges are removed.