Documentation
¶
Overview ¶
Package composition provides the public Composition Root abstraction for GoCell assemblies.
Overview ¶
A Composition Root is the single place in an application where all objects are assembled together. Before this package existed, the assembly logic was trapped inside cmd/corebundle (package main, not importable from tests or alternative entry points).
Package composition exposes three public types:
- CellModule — the per-Cell wiring contract. Each Cell declares itself to Builder.Build via one CellModule implementation.
- SharedDeps — cross-cutting dependencies shared by every CellModule. Contains Clock, JWT, metrics, event bus and capability providers.
- Builder / App — assembly entry point and runner.
Layering constraint ¶
runtime/composition is governed by the runtime-isolation depguard rule in .golangci.yml: it may import only stdlib, kernel/…, pkg/…, and the curated runtime/… + external allowlist. In particular:
- ZERO imports of adapters/…
- ZERO imports of github.com/prometheus/client_golang
If the caller needs an adapter-specific type (e.g. *promadapter.MetricProvider), it must be assigned to the appropriate kernel/runtime interface declared in SharedDeps (MetricsProvider kernel/observability/metrics.Provider).
AUTH-PLAN-04 constraint ¶
runtime/composition is forbidden from constructing auth plans (auth.NewAuthJWT / auth.NewAuthServiceToken / etc.). Listener and auth wiring must be supplied by the caller via the RuntimeOptionsFunc passed to Builder.Build. The composition root (cmd/…) owns that responsibility.
See RuntimeOptionsFunc and examples/corebundlestarter for a runnable example of supplying listener/auth wiring.
ref: uber-go/fx fx.App — single assembly entry point. ref: kubernetes-sigs/controller-runtime pkg/manager — Manager pattern.
Index ¶
- type App
- type Builder
- func (b *Builder) Build(ctx context.Context, shared *SharedDeps, runtimeOptsFn RuntimeOptionsFunc) (*App, error)
- func (b *Builder) Migrations() []MigrationRegistration
- func (b *Builder) With(modules ...CellModule) *Builder
- func (b *Builder) WithMigrations(ns migration.Namespace, fsys fs.FS) *Builder
- type CellModule
- type MigrationRegistration
- type ModuleResult
- type RuntimeOptionsFunc
- type SharedDeps
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type App ¶
type App struct {
// contains filtered or unexported fields
}
App is the assembled, ready-to-run GoCell application. It is returned by Builder.Build and started by calling App.Run.
App is intentionally thin: it delegates directly to bootstrap.New(...).Run, keeping all assembly logic in Builder.Build.
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder assembles a GoCell application from an ordered set of [CellModule]s.
ref: uber-go/fx fx.New(opts...) — single assembly entry point used by both production (main) and tests. ref: kubernetes-sigs/controller-runtime pkg/manager/manager.go — Manager accumulates options via functional options.
func New ¶
New returns a new Builder whose composed cells must form exactly the assembly's declared cell-id closed set (expectedCellIDs — the assembly.yaml cells list, threaded in by the generated entrypoint). Build fail-fasts if any composed module's ID is not in the set, if a declared cell is not provided by any module, or on duplicate module IDs (see Build).
This is the M12a build-time closed-set guard (#1093): once multi-module assembly composition exists (#1086), the assembly.yaml cell set is the authoritative enumeration of legal cell identities, and a module composing a cell outside it is a configuration bug — rejected outright, not degraded to a `_runtime` sentinel. Mirrors K8s runtime.Scheme: registration-time enumeration + hard rejection of out-of-set identities.
func NewForRole ¶
func NewForRole(assemblyCellIDs []string, spec bootstrap.DeploymentTopologySpec, allModules ...CellModule) *Builder
NewForRole returns a Builder that mounts only the cells THIS process hosts for its deployment role — the colocated subset of the assembly. It is the sanctioned subset-mount entry for production composition roots (cmd/*, examples/*): COMPOSITION-NEWFORROLE-FUNNEL-01 bans bare composition.New there so a root cannot mount the full cell set in a split topology.
spec is the per-process placement derived once at the composition root via bootstrap.SpecForRole(generatedTopologyGroups(), GOCELL_CELL_ROLE) and also carried on SharedDeps.DeploymentTopology (single source — NewForRole consumes it, it does not re-derive). Two branches:
- zero spec (all-colocated monolith): mount EVERY module unfiltered, with the full assemblyCellIDs as the closed set. Filtering here would silently drop an out-of-assembly module instead of letting Build's M12a closed-set guard (validateClosedSet) reject it; the monolith path must stay unfiltered so drift between generatedCellModules and the assembly cell list still fails.
- explicit spec (split role): the closed set is spec.Colocated. Modules whose cell is declared remote for this role are dropped (they are reached via transport); everything else — colocated modules PLUS any stranger whose cell is in neither colocated nor remote — is passed to Build, whose closed-set guard (closed set = spec.Colocated) mounts the colocated bijection and rejects a stranger ("not in the assembly closed set"). A nil module is passed through unchanged so Build's nil-guard fail-fasts on it (never silently dropped). Dropping only the known-remote set (rather than keeping only the colocated set) is deliberate: it routes stranger drift — generatedCellModules out of sync with the topology groups — into Build's fail-fast instead of a silent drop (#2278 review).
The spec is recorded on the Builder (roleSpec); Build asserts it equals SharedDeps.DeploymentTopology so the subset-mount source and the sealed runtime topology cannot diverge (single deployment-topology source).
A dropped (remote) module's Provide is never called, so a module MUST open its infrastructure in Provide, not in its constructor — a constructor that opened a connection would leak it for a remote cell this process never builds.
assemblyCellIDs is the monolith branch's independent M12a closed-set source; the split branch uses spec.Colocated. Selecting the closed set per mode is the design, not a dead parameter.
NewForRole is a convenience funnel, not the enforcement boundary. The two-sided grading: the caller-funnel COMPOSITION-NEWFORROLE-FUNNEL-01 (Medium, AST scan) bans bare composition.New in wiring roots, and the bootstrap MOUNTED-EQUALS-COLOCATED-01 phase guard (Medium, runtime bijection) is the true fail-closed backstop — it catches any process that mounts a remote cell regardless of how it was built. Bijection / missing-module / stranger errors surface from Build.validateClosedSet. Both ceilings are Medium (exported cross-package constructor + runtime cellID strings), per ADR §#1967 Amendment.
ref: akka/akka cluster-sharding withRole — one artifact, a static role selects which cells this node hosts; non-hosted cells are reached as remote.
func (*Builder) Build ¶
func (b *Builder) Build( ctx context.Context, shared *SharedDeps, runtimeOptsFn RuntimeOptionsFunc, ) (*App, error)
func (*Builder) Migrations ¶
func (b *Builder) Migrations() []MigrationRegistration
Migrations returns the registered migration sets in registration order, for the ops / composition-root entry point to apply before Build. Returns a copy so callers cannot mutate the builder's backing slice.
func (*Builder) With ¶
func (b *Builder) With(modules ...CellModule) *Builder
With appends modules to the builder. Calls are accumulating: successive With calls append, they do not replace. Analogous to fx.Provide chaining.
func (*Builder) WithMigrations ¶
WithMigrations registers an external Cell module's migration set under ns. Calls accumulate (successive calls append). The registration is validated at Builder.Build (namespace validity, NOT the reserved "platform" namespace, non-nil FS, no duplicate namespace); Build does not execute the migrations.
The reserved migration.PlatformNamespace MUST NOT be used here — it is seeded automatically by adapters/postgres.NewMigrationSetWithPlatform. Passing it is rejected fail-fast at Build (and again at adapters/postgres.MigrationSet.Add), so an external module cannot clobber the platform schema lineage.
type CellModule ¶
type CellModule interface {
// ID returns a stable identifier used in error messages and logs.
ID() string
// Provide resolves Cell-specific dependencies from the shared context and
// returns a [ModuleResult] (the constructed Cell + non-resource opts +
// the single-source ManagedResource list) or an error.
//
// Resource lifecycle is single-source: the module lists every external
// connection it opened (PG pool, vault client, …) in ModuleResult.Resources,
// and [Builder.Build] derives BOTH the happy-path bootstrap.WithManagedResource
// registration AND the pre-Run rollback from that one slice. The module MUST
// NOT call bootstrap.WithManagedResource itself.
//
// Cross-module value handoff via the former ModuleExports type has been
// removed (Wave-1 #1423). Cell modules are now fully self-contained; cross-cell
// communication happens via events (contract-based), not via in-process
// Go handle passing at composition time.
//
// MODULE-PROVIDE-NO-VALUE-HANDOFF-01 (tools/archtest/module_provide_signature_frozen_test.go)
// enforces that this signature has exactly two inputs (context.Context, *SharedDeps)
// and two outputs (ModuleResult, error), and that ModuleResult has exactly the
// fields {Cell, Opts, Resources} with no cross-module value-handoff channel.
Provide(ctx context.Context, shared *SharedDeps) (ModuleResult, error)
}
CellModule is the contract by which a Cell declares itself to Builder.Build. Each Cell provides a single *_module.go file that implements this interface, self-managing all Cell-specific dependency wiring (KeyProvider, PoolResource, cellOpts, etc.).
Cross-cell communication ¶
Cells communicate with each other exclusively via contracts: HTTP contracts (request/response) and event contracts (publish/subscribe). In-process Go handle passing between Cells at composition time is prohibited — the former ModuleExports type has been removed (Wave-1 #1423). If a Cell needs to react to another Cell's state change, it subscribes to an event contract; it does not receive a direct reference to the other Cell's internal store or service.
See MODULE-PROVIDE-NO-VALUE-HANDOFF-01 archtest for the machine enforcement.
ref: uber-go/fx fx.Module(name, opts...) — each module is self-contained and registers its own providers. ref: Go proverbs "accept interfaces, return structs" — when there is only one concrete implementation, the interface is introduced at the point where a second impl appears; CellModule has multiple impls (one per Cell) so the interface is appropriate here.
type MigrationRegistration ¶
type MigrationRegistration struct {
// Namespace identifies the migration lineage; its tracking table is
// schema_migrations_<namespace> (derived adapter-side). The reserved
// "platform" namespace is the platform's own migrations and is seeded by the
// ops entry point (adapters/postgres.NewMigrationSetWithPlatform), not here.
Namespace migration.Namespace
// FS is the embed.FS (or any fs.FS) of goose-native NNN_desc.sql files for
// this namespace, with its own independent 001..N sequence.
FS fs.FS
}
MigrationRegistration is a layer-neutral (namespace, fs.FS) migration source registered via Builder.WithMigrations. composition does NOT execute migrations — runtime/ must not depend on adapters/, and migrations run out-of-band BEFORE Build brings up cells (cells need their schema first). The registrations are surfaced via Builder.Migrations for an ops / composition-root entry point (which may import adapters/postgres) to drain into an adapters/postgres.MigrationSet and apply against a real database.
ref: docs/guides/cell-external-repo-quickstart.md "Migrations" — the execution-bridge loop external Cell modules write.
type ModuleResult ¶
type ModuleResult struct {
// Cell is the constructed Cell. Must be non-nil on success.
Cell cell.Cell
// Opts are non-resource bootstrap options the Cell needs (e.g.
// bootstrap.WithRelay). Resources MUST NOT be registered here via
// bootstrap.WithManagedResource — use the Resources field; the Builder
// derives the WithManagedResource registration from it.
Opts []bootstrap.Option
// Resources are the ManagedResources opened during Provide. The Builder
// derives both steady-state registration and pre-Run rollback from this one
// slice (single source). Entries MUST be non-nil.
Resources []kernellifecycle.ManagedResource
}
ModuleResult is the single-source result of CellModule.Provide.
It carries the constructed Cell plus the two kinds of bootstrap contribution a module can make:
- Opts: non-resource bootstrap options (e.g. bootstrap.WithRelay). These flow into the assembly verbatim.
- Resources: the ManagedResources the module opened during Provide (PG pool, vault client, rate-limiter cleanup goroutine, …). This is the SINGLE source for resource lifecycle: Builder.Build derives BOTH the steady-state registration (one bootstrap.WithManagedResource(r) per resource, so bootstrap.Run owns its Probes()/Worker()/LIFO-Close on the happy path) AND the pre-Run rollback stack (Close(ctx) in reverse order if a later module fails before bootstrap.Run starts). Because both are derived from the one slice, they can never diverge.
Modules MUST NOT call bootstrap.WithManagedResource themselves — that is forbidden inside cellmodules/ by WITHMANAGEDRESOURCE-CELLMODULE-FUNNEL-01. List the resource in Resources and the Builder funnels it. Before #1420 a module had to write the same resource into both an opt and a provisional return; forgetting either half leaked the resource or dropped its /readyz probe. The single Resources field closes that double-write.
No cross-module value-handoff field is permitted (no Exports): the former ModuleExports channel was removed in Wave-1 #1423 and MUST stay removed. The field set is frozen by MODULE-PROVIDE-NO-VALUE-HANDOFF-01.
type RuntimeOptionsFunc ¶
RuntimeOptionsFunc lets the composition root supply the fully-assembled runtime bootstrap options (assembly, three listeners + auth, consumer base, health/metrics). It receives the constructed cells so the caller can build the assembly from them.
Listener and auth construction MUST live in the caller (cmd/ or examples/), never inside runtime/composition — this package is forbidden by AUTH-PLAN-04 from constructing auth plans (auth.NewAuthJWT / auth.NewAuthServiceToken …).
Typical implementation: build a bootstrap.Assembly from cells, call auth.NewAuthJWTFromAssembly(asm) to obtain the JWT auth plan, then return bootstrap.WithAssembly(asm), bootstrap.WithListener(...) and related options.
Returns (nil, nil) if there are no runtime-specific options to add.
See examples/corebundlestarter/run.go for a runnable RuntimeOptionsFunc.
type SharedDeps ¶
type SharedDeps struct {
// service, and middleware. Tests inject clockmock.FakeClock; production
// wires clock.Real() exactly once at the entry point.
Clock clock.Clock
// must be obtained from bootstrap.NewTopology / TopologyFromEnv (it is a
// sealed type); a zero Topology fails validation.
Topology bootstrap.Topology
// this assembly (single-sourced from assembly.yaml topology, derived via
// SpecForRole(generatedTopologyGroups(), role)). It is a bootstrap.DeploymentTopologySpec
// (plain codegen input, no methods); phase0 seals it into a
// bootstrap.DeploymentTopology (with IsColocated/RemoteEndpoint); US4
// consumers query the SEALED value (via Bootstrap.DeploymentTopology()), not
// this spec. Empty spec means all cells are co-located in the same process
// (zero-migration default). Build injects it via bootstrap.WithDeploymentTopology
// so phase0 can seal+validate the spec into a runtime DeploymentTopology and
// enforce cell-placement routing. Genuinely cross-cutting: consumed by the
// composition root (Builder.Build) and bootstrap phase0 across all cell modules.
DeploymentTopology bootstrap.DeploymentTopologySpec
// Source: runtime/auth.NewJWTIssuer (via authconfig.NewJWTIssuerFromRegistry).
JWTIssuer *auth.JWTIssuer
// Source: runtime/auth.NewJWTVerifier (via authconfig.NewJWTVerifierFromRegistry).
JWTVerifier *auth.JWTVerifier
// callers assign a *promadapter.MetricProvider (which satisfies
// kernelmetrics.Provider); tests may use kernelmetrics.NopProvider{}.
//
// The field type is intentionally the interface — this package must not
// import adapters/prometheus or github.com/prometheus/client_golang.
MetricsProvider kernelmetrics.Provider
// already-persisted outbox entries to. In demo topology it is the in-process
// eventbus; in postgres topology a real broker (RabbitMQ). The type is the
// kernel/outbox interface so this package never imports runtime/eventbus or
// adapters/* (same layering reason as MetricsProvider). The composition root
// resolves it via cellmodules/eventtransport.Resolve, which gates the choice
// on Topology and fail-closes postgres mode onto a real broker (#1940).
Publisher outbox.Publisher
// In demo topology it is the SAME in-process eventbus instance as Publisher
// (so in-process publish is visible to in-process subscribers); in postgres
// topology the same broker's subscriber. Interface-typed for the same layering
// reason as Publisher.
Subscriber outbox.Subscriber
// metrics. It is genuinely cross-cell — consumed by accesscore, configcore,
// AND the corebundle config-event consumer middleware — so it stays on the
// shared bag (registered once against MetricsProvider, injected into all
// consumers). It is NOT a configcore-specific field.
ConfigEventCollector obmetrics.ConfigEventCollector
ConsumerClaimer idempotency.Claimer
// module resolves ITS pool's provider via PG.ForCell(cellID): colocated
// assemblies map every cell to one shared provider (one pool); split assemblies
// map each cell to its own pool's provider (N pools, each driven by its own
// relay). Assembly-wide consumers with no "cell" dimension (the CQRS projection
// harness) use PG.Sole(), which fails closed in split topology. Nil in
// non-postgres modes; cell modules take their in-memory path.
PG capability.PGSet
// without redis.
Redis capability.RedisProvider
// service-token signing and verification (#2153). It is an interface so the
// composition root can inject either a master-derived keyring (monolith) or a
// master-absent ProvisionedKeyring (split, per-cell). Promoted from
// cmd/corebundle's private internalGuard struct so cell modules can sign
// outbound requests without coupling to the cmd-private type.
InternalServiceKeyring kauth.ServiceKeyring
// service-token guard. Promoted from cmd/corebundle's private internalGuard
// struct (alongside InternalServiceKeyring) so that production control-plane
// validation can introspect Kind() at the composition boundary: in adapter
// mode "real" a NoopNonceStore is rejected, and an in-memory store requires
// the single-pod acknowledgement. Nil is permitted in dev/test adapter
// modes (the Kind checks are real-mode-only); see validateProductionControlPlane.
//
// Note: although validate() only checks NonceStore in real adapter mode, any
// consumer that wires the internal-listener auth chain via
// auth.NewAuthServiceToken must supply a non-nil NonceStore — that constructor
// fail-fasts on nil regardless of adapter mode. In dev/test supply
// auth.NewInMemoryNonceStore(...).
NonceStore kauth.NonceStore
PrimaryHTTPAddr string
InternalHTTPAddr string
// CellTransport holder (Epic #1423 US4 #1963). Genuinely cross-cutting: the
// composition root mints ONE holder and hands the SAME pointer to both the
// consumer cell (which dispatches co-located cross-cell http calls through it)
// and bootstrap (WithInProcessTransport), which binds the built
// internal-listener handler into it at phase5. Build always populates it.
InProcessTransport *transport.InProcessTransport
// source for distributed tracing: when non-nil, Build threads it into BOTH the
// bootstrap tracer (router + in-process transport late-bind + event router, via
// bootstrap.WithTracer) AND the remote transport (via TransportObs below), so a
// split-topology remote call gets the SAME tracer as in-process calls — closing
// the ADR D4 span half (#2251 P1.3). Nil = no tracing configured (remote and
// in-process both degrade to wrapper.NoopTracer{}); production currently leaves
// it nil (seam-only) until a composition root wires a real otel tracer. Adding
// it here (rather than a per-call argument) makes "wired metrics but forgot the
// tracer" unrepresentable at the celltransport.Resolve boundary.
Tracer wrapper.Tracer
// + Tracer) minted ONCE by Build and consumed by both the in-process transport
// and any remote transport a module resolves via celltransport.Resolve — so
// split-topology remote calls emit cell_transport_requests_total{transport_mode=
// remote} and produce spans instead of dropping them (#1966 review P1.3 / #2251,
// ADR D4). The metrics instance is reused (not re-registered) to avoid a
// duplicate-counter error. Build-populated (derived), not a caller input.
TransportObs transport.CrossCellObs
// peer cells over https (#2263). The composition root resolves it once via
// cellmodules/celltls.Resolve and hands it to celltransport.Resolve, which
// mints a per-target-cell *tls.Config (peer authenticated by SPIFFE cell ID).
// The zero value means "no client mTLS" (demo / loopback / all-colocated):
// celltransport.Resolve then fails closed only for a non-loopback https peer.
// Caller input (composition root); zero is the valid no-mTLS state.
RemoteClientTLS tlsutil.ClientIdentity
// internal listener (#2263), resolved alongside RemoteClientTLS by
// cellmodules/celltls.Resolve. When non-nil the composition root wires it via
// celltls.InternalListenerSecurity (prepends kauth.AuthMTLS{} +
// bootstrap.WithListenerTLS). Nil = no server mTLS (service-token-only, demo /
// loopback). Caller input; nil is the valid no-mTLS state. A *tls.Config (not
// a celltls type) keeps this package free of a cellmodules import (layer rule).
InternalListenerServerTLS *tls.Config
HealthHTTPAddr string
MetricsToken string
VerboseToken string
VerboseDisabled bool
// Must be false in production; set true only in tests or single-node
// deployments where the health listener is deliberately bound to a
// non-loopback address.
HealthLocalOnly bool
// Empty means devtools catalog is disabled; external callers that do not
// need devtools functionality should leave this field unset.
ProjectRoot string
// contains filtered or unexported fields
}
SharedDeps holds the cross-cutting dependencies consumed by every CellModule. It is the public, interface-only equivalent of cmd/corebundle's internal SharedDeps: all adapter-concrete types have been replaced with their kernel/runtime interface counterparts so that this package never imports adapters/ or prometheus/client_golang.
Sealed construction: SharedDeps carries an unexported validity marker (valid) that only NewSharedDeps stamps after running validate(). Builder.Build refuses any instance whose marker is unset, so a package-external struct literal — which can populate the exported fields but can never set the unexported marker — cannot be fed to Build. The single construction surface is NewSharedDeps, so an unvalidated dep set is unconstructable for the consumer. Bare literals remain legal for reads (e.g. unit tests that exercise a helper reading one field without ever calling Build); only Build gates on the marker.
Fields are flat (no concern-grouped sub-structs): SharedDeps is a composition-root bag whose fields cross consumer boundaries. Forcing a sub-struct layout would make cross-cutting consumptions look like boundary violations when in fact they are the natural shape of a composition root.
Fields are genuinely cross-cutting (consumed by multiple cells or the runtime itself). SharedDeps is now fully cell-agnostic: the former configcore-specific metric fields (EventbusCacheCollector, ConfigStaleCipherInc) were removed in #1413, and ConfigKeyProvider (the last configcore-specific field) was removed in #1413/#885 — configcore now self-builds its key provider from env + MetricsProvider, using adapters/vault.TransitMetrics which is client_golang-free in non-test code post-#885.
The exported field set is frozen by archtest SHAREDDEPS-FIELDSET-FROZEN-01 (reflect golden): adding a field requires editing that golden AND justifying the newcomer here as genuinely cross-cutting — a cell-specific dep belongs on that cell's module (constructor / self-build), not on this shared bag.
ref: uber-go/fx fx.Supply — shared values provided once to all modules. ref: kubernetes/kubernetes cmd/kube-apiserver/app/options/validation.go — all required fields validated in one place before startup.
func NewSharedDeps ¶
func NewSharedDeps(d SharedDeps) (*SharedDeps, error)
NewSharedDeps validates a populated SharedDeps and returns a sealed copy. The returned *SharedDeps carries the unexported validity marker that Builder.Build requires; a package-external SharedDeps literal cannot set it, so Build rejects any instance not produced here. This is the single construction surface for a Build-able SharedDeps.
ref: kubernetes/kubernetes cmd/kube-apiserver/app/options/validation.go — validates all fields before any component is constructed.