Skip to content

Lifetimes

wiki edited this page Sep 4, 2026 · 1 revision

Lifetimes

Every registration has exactly one lifetime, and a type may hold exactly one registration. Registering the same type twice — even under a different lifetime — returns ErrAlreadyRegistered.

Kind Constructed Shared with Closed by the container?
Singleton Once, lazily, on first resolve Everything, forever No
Scoped Once per Scope Resolves within the same scope YesScope.Close()
Transient On every Resolve call Nothing No
Instance By you, before registration Everything, forever No

Singleton

c.Singleton(func() *Database { return openDB() })

Construction is lazy and exactly once, even when several goroutines resolve it at the same moment. The practical consequence is worth stating plainly: a singleton factory may depend on anything registered before the first resolve, not merely on what was registered before the factory itself. Registration order between singletons does not matter.

Nothing closes a singleton. If it owns a connection pool, close it yourself at shutdown — the container has no shutdown of its own.

Scoped

c.Scoped(func(r dix.Resolver) *UnitOfWork { ... })

One instance per Scope, memoised for that scope's lifetime, and closed when the scope closes if it has a Close() error method. This is the lifetime for anything request-bound: a transaction, a per-request cache, a correlation ID.

Two rules follow from "per scope":

  • Resolving a scoped type from the root container fails with ErrScopedFromRoot. It does not fall back to building one. A scoped value built from the root has no owning scope, so nothing would ever close it — which is exactly the leak scopes exist to prevent.
  • A scoped factory taking a Resolver receives the scope, not the root container. Its own scoped dependencies therefore land in the same scope rather than failing.

Transient

c.Transient(func() *Encoder { return newEncoder() })

A fresh value on every Resolve, memoised nowhere. Use it for cheap, stateful, short-lived objects.

Transients are never closed, not even when resolved from a scope. The scope tracks scoped instances only. If a transient owns a resource, the caller owns closing it.

Instance

c.Instance(&Config{Env: "prod"})

Registers a value you already built. v must not be nil and must not be a function — a function would almost certainly be a factory passed to the wrong method, so it is rejected as ErrInvalidFactory rather than registered as a value of func type.

The registration key is v's dynamic type. c.Instance(myLogger) where myLogger is a *SlogLogger held in a Logger variable registers *SlogLogger, and it will satisfy a Logger resolve by interface matching.

Choosing

  • Stateless, shared, expensive to build → Singleton
  • Bound to one request or one unit of work, owns something closeable → Scoped
  • Cheap, stateful, must not be shared → Transient
  • Already built, or built by configuration code → Instance

Replacing a registration

Because a type holds only one registration, correcting one means removing the old one first:

_, _ = c.Unbind(defaultLogger) // *slogLogger
_ = c.Instance(configuredLogger) // *myLogger — a different concrete type

Unbind reports whether anything was removed. It does not close what it removes, and singletons already constructed and handed out are unaffected — holders keep the value they were given.

Clone this wiki locally