Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Develop With Ai
Structure Business Logic With Di
Upgrade Putnami
Principles
Tooling & Workspace
Workspace
Cli
Jobs & Caching
Extensions
Templates
Error Handling
Frameworks
Typescript
ExtensionOverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart ClientSchema
Go
ExtensionOverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Python
Extension
Platform
Ci
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Dependency Injection

Dependency Injection

go.putnami.dev/inject provides a hierarchical DI container with generics, named tokens, scoped providers, and cycle detection. DI is entirely optional — you can wire services explicitly and skip the container altogether.

Tokens

Tokens identify dependencies in the container. There are three kinds:

Class tokens

Derived from Go types. Most common when using constructor-based DI:

import "go.putnami.dev/inject"

token := inject.TokenOf[*UserService]()

Named tokens

For non-struct values or when you need multiple instances of the same type:

apiURL := inject.Named[string]("api-url")
dbPool := inject.Named[*sql.Pool]("primary-db")

Tag selectors

For resolving multiple providers at once:

plugins := inject.Tagged[Plugin]("plugin")

Provider registration

Factory provider

reg := inject.Provide(
    inject.TokenOf[*UserService](),
    func(r inject.Resolver) (any, error) {
        db, err := inject.ResolveAs[*Database](r, inject.TokenOf[*Database]())
        if err != nil {
            return nil, err
        }
        return NewUserService(db), nil
    },
)

Value provider

reg := inject.ProvideValue(inject.Named[string]("api-url"), "https://api.example.com")

Instance provider

reg := inject.ProvideInstance(myLogger)

Auto-provide (constructor-based)

The most concise registration. Constructor parameters are resolved by type, and the return value is registered by type:

// func NewUserService(db *Database) *UserService
reg := inject.AutoProvide(NewUserService)

Supported constructor signatures:

func() T
func() (T, error)
func(dep1 A, dep2 B) T
func(dep1 A, dep2 B) (T, error)

Auto-provide variants

// Scoped — new instance per scope (e.g., per HTTP request)
inject.AutoProvideScoped(NewRequestContext)

// Named — register under a custom name
inject.AutoProvideNamed("primary-db", NewDatabase)

Provider options

Options configure provider behavior:

inject.Provide(token, factory,
    inject.WithScope(inject.Scoped),
    inject.WithVisibility(inject.Private),
    inject.WithTags("plugin", "auth"),
    inject.WithDeps(inject.TokenOf[*Database]()),
    inject.WithOnClose(func() error { return db.Close() }),
    inject.WithLazy(),
    inject.WithDynamic(),
)
Option Default Description
WithScope(Scoped) Singleton New instance per scope instead of one global instance
WithVisibility(Private) Public Hide from child containers
WithTags(...) — Tags for multi-resolution via List()
WithDeps(...) — Explicit dependency declarations for validation
WithOnClose(fn) — Cleanup hook called on Close()
WithLazy() — Skip resolution at startup; resolve on first access
WithDynamic() — Allow refresh after startup

Container

Creating a container

root := inject.NewContainer("root", nil)
root.Register(inject.AutoProvide(NewDatabase))
root.Register(inject.AutoProvide(NewUserService))

Resolving dependencies

val, err := root.Get(inject.TokenOf[*UserService]())
userService := val.(*UserService)

// Check existence
if root.Has(inject.TokenOf[*UserService]()) {
    // registered
}

Hierarchy

Child containers inherit parent registrations. Private providers are not visible to children:

child := root.CreateChild("auth-module")
child.Register(inject.Provide(
    inject.TokenOf[*TokenValidator](),
    factory,
    inject.WithVisibility(inject.Private), // only visible in this child
))

Validation

issues := root.Validate()
for _, err := range issues {
    fmt.Println(err) // missing deps, circular deps, scope violations
}

ContainerContext

ContainerContext is the application-level entry point that adds lifecycle management and scoping on top of Container:

cc := inject.NewContainerContext("app")
cc.Register(inject.AutoProvide(NewDatabase))
cc.Register(inject.AutoProvide(NewUserService))

// Start — validates graph, resolves non-lazy singletons
if err := cc.Start(); err != nil {
    log.Fatal(err)
}

// Resolve
val, _ := cc.Get(inject.TokenOf[*UserService]())

// Close — runs onClose hooks in reverse order
defer cc.Close()

Modules

Mount a group of registrations as a child container with explicit requirements:

authRegs := []inject.Registration{
    inject.AutoProvide(NewAuthService),
    inject.AutoProvide(NewTokenValidator),
}
authReqs := []inject.Token{
    inject.TokenOf[*Database](), // must be provided by parent
}

cc.Mount("auth", authRegs, authReqs)

Scopes

Singleton (default)

One instance shared for the container's lifetime:

inject.AutoProvide(NewDatabase) // singleton by default

Scoped

New instance for each scope (e.g., per HTTP request):

inject.AutoProvideScoped(NewRequestContext)

Using scoped services

err := cc.Scope(ctx, func(ctx context.Context, scope inject.ScopeContext) error {
    reqCtx, _ := scope.Get(inject.TokenOf[*RequestContext]())
    // reqCtx is unique to this scope

    // Same scope = same instance
    reqCtx2, _ := scope.Get(inject.TokenOf[*RequestContext]())
    // reqCtx == reqCtx2
    return nil
})

Context-based resolution

Inside a scope, resolve from context.Context directly:

err := cc.Scope(ctx, func(ctx context.Context, scope inject.ScopeContext) error {
    // Typed resolution from context
    userService, err := inject.Resolve[*UserService](ctx, inject.TokenOf[*UserService]())
    if err != nil {
        return err
    }
    return userService.Process(ctx)
})

Detached scopes

For scopes with manual lifetime control:

scope, err := cc.CreateScope()
if err != nil {
    return err
}
defer scope.Close()

// Use the scope's context
scopedCtx := scope.Context(ctx)
userService, _ := inject.Resolve[*UserService](scopedCtx, inject.TokenOf[*UserService]())

Tags and multi-resolution

Tag providers for grouped resolution:

cc.Register(inject.AutoProvide(NewPluginA, inject.WithTags("plugin")))
cc.Register(inject.AutoProvide(NewPluginB, inject.WithTags("plugin")))

// Resolve all tagged providers
vals, _ := cc.List(inject.Tagged[Plugin]("plugin"))
// vals contains [PluginA, PluginB]

Inside factories, use the resolver:

inject.Provide(
    inject.TokenOf[*PluginManager](),
    func(r inject.Resolver) (any, error) {
        plugins, _ := r.ResolveAll(inject.Tagged[Plugin]("plugin"))
        return NewPluginManager(plugins), nil
    },
)

Fx-style DI with Application

The app.Application provides a higher-level API for constructor-based DI:

import "go.putnami.dev/app"

a := app.New("my-service")

// Register constructors — params resolved by type
a.ProvideFunc(
    NewDatabase,     // func() (*Database, error)
    NewUserService,  // func(db *Database) *UserService
    NewUserHandler,  // func(users *UserService) *UserHandler
)

// Scoped constructors — new instance per scope
a.ProvideScopedFunc(NewRequestContext)

// Invoke — run after DI is built
a.InvokeFunc(func(handler *UserHandler, server *fhttp.ServerPlugin) {
    server.GET("/users", handler.List)
})

a.ListenAndServe()

Scope proxies

When a singleton depends on a scoped provider, use a ScopeProxy to defer resolution:

// RequestID is scoped — new per request
// AuditLogger is singleton — constructed once

proxy := inject.ProvideScopeProxy[*RequestID](
    inject.TokenOf[*RequestID](),
    ctxFunc, // returns current context.Context
)
cc.Register(proxy)

// In the singleton, call proxy.Get() to access the current scope's instance

Testing

Fork and override

Fork a ContainerContext for test isolation:

func TestUserService(t *testing.T) {
    fork := cc.Fork()
    fork.Override(inject.TokenOf[*Database](), func(r inject.Resolver) (any, error) {
        return NewMockDatabase(), nil
    })
    fork.OverrideValue(inject.TokenOf[*EmailService](), &MockEmailService{})

    testCC, err := fork.Start()
    if err != nil {
        t.Fatal(err)
    }
    defer testCC.Close()

    val, _ := testCC.Get(inject.TokenOf[*UserService]())
    userService := val.(*UserService)
    // test with mocked dependencies
}

Manual test container

func TestUserService(t *testing.T) {
    cc := inject.NewContainerContext("test")
    cc.Register(inject.ProvideValue(inject.TokenOf[*Database](), mockDB))
    cc.Register(inject.AutoProvide(NewUserService))

    if err := cc.Start(); err != nil {
        t.Fatal(err)
    }
    defer cc.Close()

    val, _ := cc.Get(inject.TokenOf[*UserService]())
    userService := val.(*UserService)
    // test
}

Validation

The container validates the entire dependency graph during Start():

  • Missing dependencies — all declared deps must be registered
  • Circular dependencies — detected via DFS-based graph analysis
  • Scope violations — singleton depending on scoped (use scope proxies)
  • Requirement validation — mounted modules' requirements must be satisfied
cc.Register(inject.AutoProvide(NewA))  // depends on B
cc.Register(inject.AutoProvide(NewB))  // depends on A

err := cc.Start()
// Error: circular dependency: *A → *B → *A

Error codes

Code Description
inject.not_registered Token not found in container or parents
inject.circular_dependency Circular dependency detected
inject.scope_violation Singleton depends on scoped without proxy
inject.container_closed Operation on a closed container
inject.duplicate_provider Token already registered
inject.requirement_not_met Module requirement not satisfied
inject.type_mismatch Resolved value doesn't match expected type
inject.factory_failed Factory function returned an error

Related guides

  • Plugins & Lifecycle — application-level DI
  • Configuration — config-to-DI bridge
  • Testing — testing with DI

On this page

  • Dependency Injection
  • Tokens
  • Class tokens
  • Named tokens
  • Tag selectors
  • Provider registration
  • Factory provider
  • Value provider
  • Instance provider
  • Auto-provide (constructor-based)
  • Auto-provide variants
  • Provider options
  • Container
  • Creating a container
  • Resolving dependencies
  • Hierarchy
  • Validation
  • ContainerContext
  • Modules
  • Scopes
  • Singleton (default)
  • Scoped
  • Using scoped services
  • Context-based resolution
  • Detached scopes
  • Tags and multi-resolution
  • Fx-style DI with Application
  • Scope proxies
  • Testing
  • Fork and override
  • Manual test container
  • Validation
  • Error codes
  • Related guides