Lifetimes and Scopes
Manage singleton, transient, and scoped service lifetimes
Every service in Vessel has a lifecycle that determines how often instances are created and how long they live.
Singleton
One instance for the full application lifetime. This is the default lifecycle.
// Constructor-based (recommended)
vessel.Provide(c, NewDatabase, vessel.AsSingleton())
// Both calls return the same instance
db1, _ := vessel.Inject[*Database](c)
db2, _ := vessel.Inject[*Database](c)
// db1 == db2With a named registration:
vessel.ProvideNamed(c, "database", func() (*Database, error) {
return NewDatabase("postgres://localhost:5432/app"), nil
}, vessel.AsSingleton())
db1, _ := vessel.InjectNamed[*Database](c, "database")
db2, _ := vessel.InjectNamed[*Database](c, "database")
// db1 == db2Use singleton for connection pools, configuration, registries, and long-lived clients.
Transient
A new instance is created every time the service is resolved.
vessel.Provide(c, NewRequestLogger, vessel.AsTransient())
// Each call returns a different instance
log1, _ := vessel.Inject[*RequestLogger](c)
log2, _ := vessel.Inject[*RequestLogger](c)
// log1 != log2Use transient for short-lived computation objects, request builders, or anything that should not be shared.
Scoped
One instance per scope. A scope is typically created per HTTP request, but you can use scopes for any bounded operation.
vessel.Provide(c, NewRequestContext, vessel.AsScoped())
// Create a scope (e.g., in HTTP middleware)
scope := c.BeginScope()
// Within the same scope, resolution returns the same instance
ctx1, _ := vessel.Inject[*RequestContext](scope)
ctx2, _ := vessel.Inject[*RequestContext](scope)
// ctx1 == ctx2Use scoped for request-bound state, correlation data, and per-request caches.
Value Registration
Register a pre-built instance directly using ProvideValue. This is always a singleton.
config := &AppConfig{Port: 8080, Debug: true}
vessel.ProvideValue(c, config)
cfg, _ := vessel.Inject[*AppConfig](c)To register a value with a name:
vessel.ProvideValue(c, config, vessel.WithName("config"))
cfg, _ := vessel.InjectNamed[*AppConfig](c, "config")Interface Registration
Use the As constructor option to register a concrete type as an interface type.
vessel.Provide(c, NewPostgresUserRepo, vessel.As(new(UserRepository)))
// Resolve by interface type
repo, err := vessel.Inject[UserRepository](c)You can register as multiple interfaces at once:
vessel.Provide(c, NewMyService, vessel.As(new(io.Reader), new(io.Writer)))
reader, _ := vessel.Inject[io.Reader](c)
writer, _ := vessel.Inject[io.Writer](c)Lifecycle Options Summary
Control the lifecycle when calling Provide or ProvideNamed:
// Singleton (default)
vessel.Provide(c, NewDatabase, vessel.AsSingleton())
// Transient
vessel.Provide(c, NewRequestLogger, vessel.AsTransient())
// Scoped
vessel.Provide(c, NewRequestContext, vessel.AsScoped())Lifetime Guidance
| Lifetime | Use For | Example |
|---|---|---|
| Singleton | Shared, long-lived resources | DB pools, config, HTTP clients |
| Transient | Disposable, per-use objects | Request builders, temporary buffers |
| Scoped | Per-request or per-operation state | Request context, correlation IDs |
- Default to singleton for most services.
- Use transient only when sharing would cause correctness issues.
- Use scoped for anything tied to a request or bounded operation.
How is this guide?