Dependency Injection
Anvil DI is strict and boot-time oriented. It wires components before traffic is served.
The generated DI container is not used for request-time discovery. Anvil
registers providers and resolves route components during app.Wire(...).
Handlers then receive already-constructed structs.
Generated Providers
Section titled “Generated Providers”The compiler generates providers for discovered components:
- Controllers
- Groups
- Middleware
- Bundles
- Queue jobs
- GraphQL endpoints
- Services for gRPC
Generated provider keys are private implementation detail. Application code does not need them.
Generated component providers are singleton factories during the boot plan. The first successful resolve stores the value, and later resolves return the same instance.
Injection Fields
Section titled “Injection Fields”Anvil only injects fields that carry an inject tag. This keeps wiring
explicit and prevents normal exported fields from being mistaken for
dependencies.
type Projects struct { sdk.Controller `path:"/projects"`
Store project.Store `inject:""`}Use inject:"" for the default provider of that Go type. Use
inject:"name" when the field should resolve a named provider.
Untagged exported fields are left alone. They can be ordinary component state, group children, route tables, or anything else your application owns.
Application Providers
Section titled “Application Providers”Applications provide infrastructure:
app := anvil.New( httpstd.Driver(), anvil.WithProviders( anvil.As[project.Store](project.NewPostgresStore(db)), anvil.As(config), ),)Use As[T] with an explicit type argument when the injection point is an
interface. Without the type argument, Go infers T from the value expression,
which usually means the concrete type:
// Field type is project.Store, so register the interface key.anvil.As[project.Store](project.NewPostgresStore(db))
// Field type is *Config, so inference is fine.anvil.As(&Config{Addr: ":8080"})The provider value must match the type Anvil resolves. If a generated field is
*Config, register anvil.As[*Config](cfg). If the field is an interface,
register the interface type explicitly:
anvil.As[project.Store](project.NewPostgresStore(db))Provider keys are built from the named Go type and optional provider name. Anvil uses reflection to compute those keys during startup configuration, not during request handling. Pointer types use the named element type for the key, but the stored value still keeps its actual Go type for the final type assertion.
This is the only reason provider helpers use reflection: they need the package
path and type name for T. The generated route path does not resolve
dependencies by reflection while a request is being handled.
As, Named, Factory, and NamedFactory require T to resolve to a named
Go type after pointers are removed. Built-in types, anonymous structs, and
unnamed slices or maps are rejected as provider keys. Named types are accepted
even when their underlying type is a slice, map, or struct, because the key can
still use the package path and type name.
Custom providers can implement sdk.Provider directly. Generated wiring can
resolve only providers with non-empty keys. If a custom provider returns an
empty key, app.RegisterProvider calls Build immediately and does not store
the result in the generated DI graph.
Named Dependencies
Section titled “Named Dependencies”If two fields intentionally use the same Go type, name both ends:
type Services struct { sdk.Bundle
ReadStore project.Store `inject:"read"` WriteStore project.Store `inject:"write"`}app := anvil.New( anvil.WithProviders( anvil.Named[project.Store]("read", readStore), anvil.Named[project.Store]("write", writeStore), ),)Anvil does not pick a random store. When two dependencies share a Go type, name the dependency on both the provider and the injection field.
Provider names become part of the key as package.Type#name. Empty provider
names are rejected for Named and NamedFactory.
Factories
Section titled “Factories”Factories build values from other registered providers during wiring:
anvil.Factory(func(r sdk.DependencyResolver) (*Service, error) { store, err := anvil.Resolve[project.Store](r) if err != nil { return nil, err } return NewService(store), nil})Factories return errors for normal wiring failures. Reserve MustResolve for
cases where failing fast during wiring is intentional.
Factories receive sdk.DependencyResolver, not the internal DI container. Use
anvil.Resolve[T](r) or anvil.NamedResolve[T](r, name) inside factories.
Factories are singleton factories in normal generated wiring. If two components depend on the same factory-backed value during the boot plan, the first successful resolve stores the value and later resolves reuse it.
The DI container is safe to call concurrently, but it is not a singleflight guard for provider side effects. If application code manually resolves the same missing key from multiple goroutines at the same time, the factory can execute more than once before one value is cached. Generated wiring resolves dependencies before transports start, so keep provider factories startup-only and deterministic.
If a factory returns nil, a typed resolve returns the zero value for the target
type. If it returns the wrong concrete Go type for the field being resolved,
Wire returns an error when that dependency is reached by generated wiring.
Keep factories deterministic and side-effect aware. A factory can be called while the app is wiring, before transports start. Return an error for configuration failures instead of starting background work that lives outside Anvil’s lifecycle hooks.
Cycles
Section titled “Cycles”Anvil treats dependency cycles as configuration bugs. The container fails fast instead of producing a partially wired application.
Cycle failures return di.CycleError with a path such as:
di: cyclic dependency: service -> repo -> serviceMustResolve panics by design, but generated wiring and user factories normally
return errors. Return the error from Wire or the provider factory so startup
can fail cleanly.
Wiring Time
Section titled “Wiring Time”app.Wire(...) performs the generated boot plan:
- Register generated providers in the DI container.
- Resolve required protocol transports from the app.
- Resolve route, middleware, GraphQL, gRPC, and queue components from DI.
- Register protocol metadata with the selected drivers.
If a provider is missing, returns the wrong Go type, or participates in a
cycle, Wire returns an error before the listener starts. Duplicate provider
keys fail at registration time: application providers fail while anvil.New
applies options, and generated providers fail when app.Wire(...) registers
the generated provider list.
The DI container stores singleton instances after the first successful resolve. Generated wiring resolves components during boot and passes the constructed values into driver route metadata, so handler dispatch does not ask the container to discover dependencies.
The container does not recover provider panics. Provider code returns errors
for configuration and construction failures. MustResolve is the
intentional panic escape hatch for startup-only code that wants a hard fail.