Skip to content
SHC Docs

ADR-0006 — Thread registry.Deps through every seam; no global root pointer

  • Status: Accepted + COMPLETE (2026-06-22). di-singletons reached 0 — every blocker-seam type below was converted the correct way (auth middleware factory, Diagnoses/TaskOn Deps-forms, bare helpers via injected resolvers, CLI local construction, the clone HTTP router de-locatored — both its PULL singleton and its PUSH SetServiceResolver global deleted, and finally the app module: the ServiceRecorder threaded through ComposeRunOptions so no in-daemon install path drops service-row recording, the state-mirror + CLI readers onto an injected resolveAppService, the ~15 daemon adapters onto a captured root *Services, and both app singletons deleted in d6e540757). The wave was adversarially verified (zero findings). app’s Set/GetRuntimeStackRepo + SetIngressIntentRecorder are SEPARATE singleton families (different names, not counted by the ratchet) and were intentionally left. The arch-fitness.sh ratchet (now hard-zero) keeps it there.
  • Builds on: ADR-0001 (Pure DI at a composition root). Enforced by scripts/arch-fitness.sh di-singletons (→ 0).

The DI sweep deleted 19 of 30 package singletons cleanly. The final ~9 modules resisted because some of their readers are installed with no registry.Deps to reach the composition root — five seam shapes:

  1. chi auth middlewareauth.Authenticate / RequirePermissions / OptionalAuthenticate installed as bare package-function values across many routers (middleware.go).
  2. registry.DiagnosesDiagnosis.Run func(ctx) carries no Deps (core/registry/types.go); used by vault/doctor.go.
  3. task.TaskOn handlersfunc(ctx, kwargs) signature, no Deps; used by network, notification, health crons.
  4. bare exported helpers reading a singleton — cluster.GetNodeRegistry, vault.ResolveAutogenViaVault — called package-wide.
  5. standalone-CLI bodiesforward.RunForward (the shc forward subprocess).

Two ways to give these access to the root were considered: thread Deps properly, or publish a single boot-time atomic.Pointer[Services] that the no-Deps sites read ambiently.

Thread registry.Deps through every seam — the way Kubernetes, Terraform, etcd, and Docker do. Do NOT introduce a global root pointer / ambient root accessor.

A boot-published atomic.Pointer[Services] is precisely the service-locator anti-pattern this whole migration removes (ADR-0001). Reintroducing it for the last mile would regress the architecture at the finish line — the mature Go infra projects are famous specifically for not doing this. They thread dependencies even when it means changing a registration contract, because the alternative (ambient global) is the thing they reject.

The fix per seam is a constructor/factory form — the registration takes a func that receives deps and returns the handler, built at the wire site that holds Deps:

SeamMature-Go patternSHC fix
chi middlewarek8s genericapifilters.WithAuthentication(h, authn, …) — authenticator passed inauth.Authenticate(svc) func(http.Handler) http.Handler, built at each router from d.Services
registry.Diagnosesk8s controllers: a struct constructed with deps; Reconcile closes over thema Deps-carrying registration form; the diagnosis builder captures the resolved *Service
task.TaskOnTerraform ConfigureContextFunc returns a configured metaa Deps-carrying handler-builder; the dispatcher (wired with the root) supplies Deps
bare helpersk8s injects a NodeLister, never a cluster.GetNodeRegistry() globaldelete the global-reader; inject a resolver at the caller
standalone CLIeach cmd/ binary has its own composition rootRunForward constructs its *Service locally

The two existing lazyConfigRoot / lazyEventsRoot atomics are the sole, narrow exception — they serve boot goroutines that genuinely run before newServices() exists (runDBConfigReload, operation_event_bridge). They are documented, bounded, and must not be extended to the seams above; no new ambient-root accessor is permitted.

  • Churn is accepted, deliberately. Changing registry.Diagnoses / task.TaskOn ripples to all diagnoses/crons; the auth-middleware factory touches every router that installs auth. This is the cost of correctness and it is paid once.
  • End state: di-singletons → 0. Zero service-locator globals; every dependency flows from the composition root by construction — the Kubernetes/Terraform shape.
  • The arch-fitness.sh ratchet keeps it there; a future re-introduced global GetRuntimeService (or a new ambient root pointer) fails make lint.
  • Boot-published atomic.Pointer[Services] for the last mile — pragmatic but it is the service locator; rejected for regressing ADR-0001 at the finish line.
  • Leave the 9 behind the dual-write bridge at 11 — a real improvement, but stops short of the correct end state with no architectural reason to.