Dependency Injection

Register and resolve services with Forge's built-in DI container

Forge includes a full dependency injection container powered by Vessel. Services are registered with constructors or factories, resolved by name or type, and managed across three lifetime scopes. The DI container handles startup ordering, shutdown cleanup, and circular dependency detection automatically.

Service Lifetimes

Every registered service has a lifetime that determines when instances are created and how long they live:

LifetimeDescriptionUse Case
SingletonCreated once, shared across the entire applicationDatabase connections, configuration, caches
TransientCreated fresh on every Resolve callRequest-specific processors, one-off workers
ScopedCreated once per scope (e.g., per HTTP request)Request-scoped state, per-request database transactions

Registering Services

Forge offers two registration patterns: constructor injection (type-based, recommended) and named registration (factory-based, backward-compatible).

Constructor Injection with Provide

The recommended pattern uses forge.Provide to register constructors whose parameters are resolved by type automatically:

// Define constructors as plain functions.
func NewDatabase(cfg *Config) (*Database, error) {
    return sql.Open(cfg.Driver, cfg.DSN)
}

func NewUserService(db *Database, log forge.Logger) *UserService {
    return &UserService{db: db, log: log}
}

// Register constructors -- dependencies are auto-resolved by type.
forge.Provide(app.Container(), NewDatabase)
forge.Provide(app.Container(), NewUserService)

// Resolve by type (no string keys needed).
userService, err := forge.Inject[*UserService](app.Container())

Use vessel options to control the registration:

import "github.com/xraph/vessel"

// Register as a named singleton.
forge.Provide(app.Container(), NewPrimaryDB, vessel.WithName("primary"), vessel.AsSingleton())

// Register into a group.
forge.Provide(app.Container(), NewEmailNotifier, vessel.AsGroup("notifiers"))

forge.ProvideConstructor is a deprecated alias for forge.Provide. Use forge.Provide in new code.

Register a Pre-Built Value

For instances that already exist (no factory needed), use ProvideValue:

cfg := &AppSettings{Debug: true, MaxRetries: 3}
forge.ProvideValue(app.Container(), cfg)

// Resolve by type.
settings, err := forge.Inject[*AppSettings](app.Container())

You can also register a named value:

import "github.com/xraph/vessel"

forge.ProvideValue(app.Container(), cfg, vessel.WithName("appSettings"))

// Resolve by name.
settings, err := forge.InjectNamed[*AppSettings](app.Container(), "appSettings")

Named Registration (Factory Pattern)

For cases where you need a name-based factory pattern (e.g., resolving services by key), use the generic RegisterSingleton, RegisterTransient, or RegisterScoped helpers:

// RegisterSingleton[T] -- created once, shared globally.
forge.RegisterSingleton[*UserRepository](app.Container(), "userRepo",
    func(c forge.Container) (*UserRepository, error) {
        db, err := forge.Inject[*sql.DB](c)
        if err != nil {
            return nil, err
        }
        return NewUserRepository(db), nil
    },
)
// RegisterTransient[T] -- new instance on every resolve.
forge.RegisterTransient[*RequestLogger](app.Container(), "requestLogger",
    func(c forge.Container) (*RequestLogger, error) {
        return NewRequestLogger(), nil
    },
)
// RegisterScoped[T] -- one instance per scope (e.g., per HTTP request).
forge.RegisterScoped[*Transaction](app.Container(), "transaction",
    func(c forge.Container) (*Transaction, error) {
        db, err := forge.Inject[*sql.DB](c)
        if err != nil {
            return nil, err
        }
        tx, err := db.Begin()
        return &Transaction{tx: tx}, err
    },
)

Register a Named Value

Use RegisterValue to store a pre-built instance under a name:

cfg := &AppSettings{Debug: true}
forge.RegisterValue[*AppSettings](app.Container(), "settings", cfg)

// Resolve by name.
settings, err := forge.Resolve[*AppSettings](app.Container(), "settings")

Using app.RegisterService

The application-level helper registers a named service with lifetime options:

app := forge.New(forge.WithAppName("my-api"))

app.RegisterService("userRepo", func(c forge.Container) (any, error) {
    db, err := forge.Inject[*sql.DB](c)
    if err != nil {
        return nil, err
    }
    return NewUserRepository(db), nil
}, forge.Singleton(), forge.WithDependencies("database"))

Resolving Services

By Type

When a service is registered with forge.Provide, resolve it by type:

db, err := forge.Inject[*Database](app.Container())
userService, err := forge.Inject[*UserService](app.Container())

By Name

When a service is registered with a name (via RegisterSingleton, RegisterValue, or vessel.WithName):

repo, err := forge.Resolve[*UserRepository](app.Container(), "userRepo")
settings, err := forge.Resolve[*AppSettings](app.Container(), "settings")

In Handlers

Access the container through the request context to resolve services inside HTTP handlers:

app.Router().GET("/users/:id", func(ctx forge.Context) error {
    repo, err := forge.Inject[*UserRepository](ctx.Container())
    if err != nil {
        return forge.InternalError("service unavailable")
    }

    user, err := repo.FindByID(ctx.Request().Context(), ctx.Param("id"))
    if err != nil {
        return forge.NotFound("user not found")
    }

    return ctx.JSON(200, user)
})

In Lifecycle Hooks

Resolve services from the app's container during startup:

app.RegisterHookFn(forge.PhaseAfterRegister, "seed-data",
    func(ctx context.Context, app forge.App) error {
        repo := forge.MustInject[*UserRepository](app.Container())
        return repo.SeedDefaultUsers(ctx)
    },
)

Must vs Safe Resolution

FunctionBehavior on Error
forge.Inject[T](c)Returns (T, error) -- safe for runtime use
forge.MustInject[T](c)Panics on error -- startup only
forge.Resolve[T](c, name)Returns (T, error) -- named resolution
forge.Must[T](c, name)Panics on error -- named, startup only

Only use Must and MustInject during application startup (in main() or lifecycle hooks) where a panic is acceptable. In HTTP handlers, always use Inject or Resolve and handle the error gracefully.

Built-In Services

Forge automatically registers these services in the container at startup. You can resolve them by type or by name:

ServiceKeyHelper FunctionType
Logger"forge:logger"forge.GetLogger(c)forge.Logger
Config Manager"forge:config"Direct via app.Config()forge.ConfigManager
Metrics"forge:metrics"forge.GetMetrics(c)forge.Metrics
Health Manager"forge:health"forge.GetHealthManager(c)forge.HealthManager
Router"forge:router"forge.GetRouter(c)forge.Router

These services are also registered as constructor-injectable types, so you can resolve them by type directly:

logger, err := forge.Inject[forge.Logger](app.Container())

Named Instances

When you have multiple instances of the same type, use names to distinguish them:

import "github.com/xraph/vessel"

forge.Provide(app.Container(), NewPrimaryDB, vessel.WithName("primary"))
forge.Provide(app.Container(), NewReplicaDB, vessel.WithName("replica"))

primary, err := forge.InjectNamed[*Database](app.Container(), "primary")
replica, err := forge.InjectNamed[*Database](app.Container(), "replica")

Service Groups

Register multiple implementations under a group and resolve them all at once:

import "github.com/xraph/vessel"

forge.Provide(app.Container(), NewEmailNotifier, vessel.AsGroup("notifiers"))
forge.Provide(app.Container(), NewSlackNotifier, vessel.AsGroup("notifiers"))
forge.Provide(app.Container(), NewSMSNotifier, vessel.AsGroup("notifiers"))

// Resolve all notifiers.
notifiers, err := forge.InjectGroup[Notifier](app.Container(), "notifiers")
for _, n := range notifiers {
    n.Send("Application started")
}

Lazy References

Lazy references defer resolution until the value is first accessed. Use them to break circular dependencies or defer expensive initialization:

// Lazy reference -- resolved on first .Get() call.
cacheRef := forge.NewLazyRef[*CacheClient](app.Container(), "cache")

cache, err := cacheRef.Get()
if err != nil {
    // Handle cache unavailable.
}

Optional Lazy Reference

Returns a zero value without error if the dependency is not found:

optRef := forge.NewOptionalLazyRef[*FeatureFlags](app.Container(), "featureFlags")
flags, _ := optRef.Get() // flags may be nil

Provider Reference

Creates a new instance on each .Get() call (transient-like behavior):

providerRef := forge.NewProviderRef[*Worker](app.Container(), "worker")

w1, _ := providerRef.Get() // fresh instance
w2, _ := providerRef.Get() // different instance

Type Checking

Check if a service is registered before resolving:

if forge.HasType[*CacheClient](app.Container()) {
    cache, _ := forge.Inject[*CacheClient](app.Container())
    // Use cache.
}

if forge.HasTypeNamed[*Database](app.Container(), "replica") {
    // Replica database is available.
}

Registration Options

When registering services through app.RegisterService(), you can pass additional options:

app.RegisterService("myService", factory,
    forge.Singleton(),                           // Lifetime
    forge.WithDependencies("database", "cache"), // Explicit dependency declaration
    forge.WithGroup("handlers"),                 // Add to a named group
    forge.WithDIMetadata("version", "2.0"),      // Diagnostic metadata
)
OptionDescription
forge.Singleton()Service is created once and shared (default)
forge.Transient()Service is created on every resolve
forge.Scoped()Service is created once per scope
forge.WithDependencies(names...)Declares dependencies for startup ordering
forge.WithGroup(name)Adds the service to a named group
forge.WithDIMetadata(key, value)Attaches diagnostic metadata

For constructor-based registration with forge.Provide, use vessel options directly:

OptionDescription
vessel.WithName(name)Registers the service under a name
vessel.WithAliases(names...)Registers additional name aliases
vessel.AsSingleton()Singleton lifetime (default)
vessel.AsTransient()Transient lifetime
vessel.AsScoped()Scoped lifetime
vessel.AsGroup(name)Adds the service to a named group
vessel.WithEager()Eagerly create the service at container startup

API Reference

Registration

FunctionDescription
forge.Provide(c, constructor, opts...)Register a constructor with auto-resolved dependencies
forge.ProvideValue[T](c, value, opts...)Register a pre-built value
forge.RegisterSingleton[T](c, name, factory)Register a named singleton factory
forge.RegisterTransient[T](c, name, factory)Register a named transient factory
forge.RegisterScoped[T](c, name, factory)Register a named scoped factory
forge.RegisterValue[T](c, name, value)Register a named pre-built value

Resolution

FunctionDescription
forge.Inject[T](c)Resolve by type
forge.MustInject[T](c)Resolve by type or panic
forge.InjectNamed[T](c, name)Resolve by name
forge.MustInjectNamed[T](c, name)Resolve by name or panic
forge.Resolve[T](c, name)Alias for InjectNamed
forge.Must[T](c, name)Alias for MustInjectNamed
forge.InjectGroup[T](c, group)Resolve all services in a group

Lazy Wrappers

FunctionDescription
forge.NewLazyRef[T](c, name)Deferred resolution on first .Get()
forge.NewOptionalLazyRef[T](c, name)Deferred resolution, nil if not found
forge.NewProviderRef[T](c, name)Fresh instance on each .Get()

Type Checks

FunctionDescription
forge.HasType[T](c)Check if a type is registered
forge.HasTypeNamed[T](c, name)Check if a named type is registered

Next Steps

How is this guide?

On this page