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:
| Lifetime | Description | Use Case |
|---|---|---|
| Singleton | Created once, shared across the entire application | Database connections, configuration, caches |
| Transient | Created fresh on every Resolve call | Request-specific processors, one-off workers |
| Scoped | Created 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
| Function | Behavior 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:
| Service | Key | Helper Function | Type |
|---|---|---|---|
| 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 nilProvider 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 instanceType 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
)| Option | Description |
|---|---|
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:
| Option | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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
| Function | Description |
|---|---|
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?