Skip to content

Service Lifecycle

Every service is either shared — built once and cached on the container — or non-shared, built anew on every access. shared: true is the default.

Shared Services (Singletons)

services:
  database:
    constructor:
      func: "db.New"
    shared: true  # Default
  • Created once on first access
  • Same instance returned on subsequent calls
  • Construction is serialized by the container mutex (see Locking)
  • Suitable for: databases, HTTP clients, loggers

Non-Shared Services (Factories)

services:
  request_context:
    constructor:
      func: "ctx.New"
    shared: false
  • New instance created on each access
  • No caching; the instance is never stored on the container
  • Suitable for: request handlers, temporary objects

When the Cache Disappears

A shared service that is neither public nor an alias, and that exactly one service depends on, is generated without a cache field: with a single holder anchored by a shared ancestor, caching is unobservable, so the generator drops it. The guarantee that survives is the observable one — that holder still sees one instance for the lifetime of the container.

This is why reading the generated file can suggest that shared: true was ignored. It was not; there was simply nothing for the cache to protect.

Locking

The container has exactly one mutex, and only the public getters take it — for both lifecycles. It guards the shared-instance fields of the whole graph, not just the service being fetched, so one public getter call builds its entire subgraph under a single lock.

The internal getters services use to reach each other are lock-free, including those of shared services: they run only inside a public getter that already holds the mutex. The mutex is a plain sync.Mutex and is not reentrant, so a constructor must not call a public getter of its own container — that deadlocks. Take dependencies as constructor arguments instead.