0002 — Explicit wiring, no dependency-injection container
Status: accepted, 2026-09-02. The record is
docs/adr/0002-explicit-wiring.md;
this page is its map and its summary and states nothing the record does not.
The map
decisions/0002-explicit-wiring in the recordContext
The previous codebase composed modules with uber/fx behind a 34-field module
descriptor and eleven layers of materialization, about 5,900 lines between a
module’s module.go and fx.New, with 32 distinct fx groups and 91 registry
types. Wiring errors surfaced at boot, as reflection errors naming interfaces
rather than call sites, and reading the graph meant running the program.
Decision
apps/platformkit/main.go constructs modules in dependency order and passes
each one a struct of typed dependencies: no container, no groups, no
registries, no reflection. A module’s dependencies are the fields of its Deps
struct, and a missing or mistyped one is a compile error at the construction
site. Cross-module dependencies remain interfaces declared in the provider’s
contracts/ package, so a module still depends on a capability rather than an
implementation and a fake still satisfies the same conformance suite as the
real thing; what is deleted is the machinery that used to connect them.
Consequences
-
The wiring graph is readable top to bottom in one file, and
go buildchecks it. -
Startup order is written down instead of derived, which makes it reviewable and makes cycles impossible to express.
-
main.gogrows with the catalog, roughly one line per module. That is the price of the graph being visible, and theapps/line ceiling caps it. -
Lifecycle hooks (start/stop) are ordinary method calls the kernel makes, not framework callbacks.
Where it lives
-
apps/platformkit/main.go—runloads the configuration, callscompose, and hands the module list toapp.Newwith the tenant, authorizer and authenticator implementations the kernel cannot choose for itself. -
apps/platformkit/modules.go—compose: oneModule(Deps{…})call per module in dependency order,auditnext to last andadminlast, and the adapters (recipients,tenantHosts,firstAdmin,seedRoles) that satisfy one module’scontracts/interface over another module’s service. -
kit/module/module.go—module.Module, the manifest a constructor returns;module.Validate, which checks names, permissions, events, subscriptions, nav entries and jobs and reports every violation in one error. -
kit/app/app.go—app.Newexpands and validates the composition before anything is opened;Runlets every module register its routes and runsapi.ValidateDeclarations,validatePermissionsandvalidateEventsbefore anything listens. -
modules/auth/module.go—auth.Deps, seven typed fields — four interfaces fromcontracts/packages, ajobs.TenantLister, theOIDCprovider andPublicHost— every one a valuemaindecides;Module(deps Deps)returns the service and the manifest. Every module follows the shape, and an emptyDepsis a module that needs nothing. -
scripts/check_imports.sh— every cross-module import is acontracts/import, andapps/never reaches a module’sinternal/; it is the last line ofmake check.
Evidence
grep -r 'go.uber.org/fx' go.mod # no output: the dependency does not exist
The record names no test, because the check is the compiler: a missing or
mistyped Deps field fails go build at the construction site before any
test runs. What the compiler cannot see, a module importing another module’s
implementation rather than its contracts/, is what
scripts/check_imports.sh refuses.