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 == db2

With 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 == db2

Use 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 != log2

Use 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 == ctx2

Use 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

LifetimeUse ForExample
SingletonShared, long-lived resourcesDB pools, config, HTTP clients
TransientDisposable, per-use objectsRequest builders, temporary buffers
ScopedPer-request or per-operation stateRequest 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?

On this page