Skip to content
Dependency Injection

Dependency Injection

Overview

The di package provides a full-featured dependency injection container with support for three service lifetimes, automatic dependency resolution, circular dependency detection, and scoped containers.

Service Lifetimes

Three lifetimes determine when a service instance is created and how long it lives:

LifetimeBehavior
TransientA new instance is created every time the service is resolved
SingletonA single instance is created and shared for the lifetime of the container
ScopedA single instance is created per scope (e.g., per HTTP request)

Basic Usage

import "github.com/lemmego/api/di"

container := di.New()

Registration

// Singleton — one instance for the application lifetime
di.RegisterSingleton[*Database](container, func() *Database {
    return NewDatabase()
})

// Transient — new instance each time
di.RegisterTransient[*UserService](container, func(db *Database) *UserService {
    return NewUserService(db)
})

// Or register with explicit lifetime
di.Register[*UserService](container, di.Scoped, func() *UserService {
    return NewUserService()
})

Resolution

db, err := di.Resolve[*Database](container)
userSvc := di.MustResolve[*UserService](container)

Check Registration

if di.Has[*UserService](container) {
    svc := di.MustResolve[*UserService](container)
}

Automatic Dependency Resolution

Factory functions can declare dependencies as parameters. The container resolves them automatically:

di.RegisterSingleton[*Database](container, func() *Database {
    return NewDatabase("postgres://...")
})

di.RegisterTransient[*UserRepository](container, func(db *Database) *UserRepository {
    return NewUserRepository(db)
})

di.RegisterTransient[*UserService](container, func(repo *UserRepository) *UserService {
    return NewUserService(repo)
})

// Resolving UserService automatically resolves Database and UserRepository
svc := di.MustResolve[*UserService](container)

Fluent API

The fluent For[T] API provides a cleaner registration syntax:

di.For[*Database](container).
    AsSingleton().
    Use(func() *Database {
        return NewDatabase("postgres://...")
    })

di.For[*UserService](container).
    AsTransient().
    Use(func(db *Database) *UserService {
        return NewUserService(db)
    })

// Register an existing instance
di.For[*Config](container).
    AsSingleton().
    UseInstance(&Config{Port: 8080})

Scoped Containers

Scoped services are useful for per-request dependencies:

// Register a scoped service
di.For[*RequestContext](container).
    AsScoped().
    Use(func() *RequestContext {
        return &RequestContext{RequestID: uuid.New().String()}
    })

// Create a scope (e.g., per HTTP request)
scope := container.CreateScope()

// Resolving within the scope returns the same instance
ctx1 := di.MustResolve[*RequestContext](scope)
ctx2 := di.MustResolve[*RequestContext](scope)
// ctx1 == ctx2 (same instance within scope)

// Different scope = different instance
scope2 := container.CreateScope()
ctx3 := di.MustResolve[*RequestContext](scope2)
// ctx3 != ctx1 (different scope)

Circular Dependency Detection

The container detects circular dependencies at resolution time and returns an error instead of entering an infinite loop.

Container Access

The container instance itself can be injected into factories:

di.For[*ServiceLocator](container).
    AsSingleton().
    Use(func(c *di.Container) *ServiceLocator {
        return &ServiceLocator{container: c}
    })

When to Use DI

The di package is suitable for complex applications that need:

  • Multiple service lifetimes (transient, singleton, scoped)
  • Automatic dependency resolution
  • Scoped request-based services
  • Fluent registration syntax

For simpler cases, the service container (app.AddService / app.Get[T]) is sufficient.