Skip to content

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.

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.

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.

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.

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 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.

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 -> service

MustResolve 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.

app.Wire(...) performs the generated boot plan:

  1. Register generated providers in the DI container.
  2. Resolve required protocol transports from the app.
  3. Resolve route, middleware, GraphQL, gRPC, and queue components from DI.
  4. 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.