---
title: Compose dependencies and own their scopes
editUrl: https://github.com/Cratis/Fundamentals.Go/edit/main/Documentation/dependency-injection.md
description: Wire Go constructors directly or use exact typed bindings, explicit scopes and deterministic resource cleanup.
---


<!-- Copyright (c) Cratis. All rights reserved. -->
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->

When several handlers share dependencies, the difficult part is not calling a
constructor: it is deciding how long its result lives and who closes it. Start
with ordinary constructors. Use the optional container when you want shared
lifetime management, graph validation and coordinated cleanup. Both paths use
Go 1.26 or newer and only the standard library.

## Wire constructors directly

You do not need a container to use Fundamentals.Go or to supply dependencies to
a framework. Construct the values yourself and close any resources you own:

```go
package main

import "fmt"

type report struct{ title string }

func newReport(title string) *report { return &report{title: title} }

func main() {
    value := newReport("Quarterly report")
    fmt.Println(value.title)
}
```

Run `go run .` to print:

```text
Quarterly report
```

The [plain-constructor example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/example_test.go)
checks this program and output. Plain constructors keep dependency and ownership
decisions visible. For custom integration, implement `di.Resolver` for exact-key
lookup and `di.ScopeFactory` to open and close your own scopes. The
[manual resolver and scope factory examples](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/example_test.go)
show this path without importing the container.

Import the contracts as `di` from
`github.com/cratis/fundamentals.go/dependencyinjection`. A custom scope needs only
`Resolve(context.Context, di.Key) (any, error)` and `Close(context.Context) error`.
Use `di.Resolve[T]` to validate its results; nil, typed-nil and incompatible
values become errors rather than type-assertion panics.

## Register constructors with the container

The default container adds caching, declared-graph checks and disposal. This
complete program uses parameter types to declare the dependency edge:

```go
package main

import (
    "context"
    "errors"
    "fmt"

    di "github.com/cratis/fundamentals.go/dependencyinjection"
    "github.com/cratis/fundamentals.go/dependencyinjection/container"
)

type title string
type report struct{ title title }

func newReport(_ context.Context, value title) (*report, error) {
    return &report{title: value}, nil
}

func run(ctx context.Context) error {
    var registry container.Registry
    if err := di.BindValue(&registry, title("Quarterly report")); err != nil {
        return err
    }
    if err := di.BindFunc1(&registry, di.Scoped, newReport); err != nil {
        return err
    }
    p, err := registry.Build()
    if err != nil {
        return err
    }
    s, err := p.NewScope(ctx)
    if err != nil {
        return errors.Join(err, p.Close(context.Background()))
    }
    value, resolveErr := di.Resolve[*report](ctx, s)
    if resolveErr == nil {
        fmt.Println(value.title)
    }
    return errors.Join(resolveErr, s.Close(context.Background()), p.Close(context.Background()))
}

func main() {
    if err := run(context.Background()); err != nil {
        panic(err)
    }
}
```

Run the program with `go run .`:

```text
Quarterly report
```

The [container example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/container/example_test.go)
checks this complete program and output.

`Build` runs no factories, guards or closers. It rejects missing declared edges,
cycles and singleton paths that reach scoped services, including through
transients. Successful Build freezes the registry; failed validation leaves it
editable. Duplicate registrations fail rather than silently replacing values.

`BindFunc1` through `BindFunc4` derive direct edges from constructor parameters
and resolve arguments in order. Repeated parameter types declare one edge.
Borrowed variants use the same signatures. For unusual wiring or generated
code, use `di.Bind` or `di.BindBorrowed` with a `di.Factory[T]` and explicit
`di.KeyFor[A]()` dependencies. `di.NewBinding` creates an opaque descriptor for
custom registrars; accessors and `Validate` let an adapter inspect it, while
`Construct` invokes and validates its result without owning cleanup.

## Convention-based bindings

When a product generator wires many services, you can share constructor rules
without sharing a generator. `dependencyinjection/bindingtypes` analyzes already
type-checked packages and returns a registration plan. It never loads packages,
executes constructors, touches files or emits code. Arc and Chronicle own their
renderers and artifact discovery; Fundamentals supplies only the planner.
For a complete type-check → directive → plan example, configuration fields and
diagnostic recovery, follow [Plan constructor bindings](/fundamentals/go/constructor-bindings/).
The rules below remain the composition and ownership contract.

Put lifetime directives on type doc comments, not constructors. This excerpt
from the [render fixture](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/internal/bindingcorpus/render/render.go)
requests one resource per scope:

```go
// Foo records close calls.
//cratis:scoped
type Foo struct{ Closes int }
```

`//cratis:singleton` requests provider caching; `//cratis:ignore-convention`
excludes a type from DI conventions only. No lifetime directive means transient.
`ReadDirectives(files, info)` normalizes these comments into `TypePolicy` values.
Parse with `parser.ParseComments` and populate `types.Info.Defs`. Grouped type
blocks need per-type doc comments. Unknown, malformed, misplaced, repeated or
conflicting directives are errors. Export data drops comments: supply source
metadata or explicit policies for external packages rather than inferring their
lifetimes.

Pass packages from one coherent importer universe to `Analyze(pkgs, cfg)`.
`Config.EmitPackage` controls accessibility and defaults to the sole input
package; supply it explicitly when analyzing multiple packages. A nil
`Constructors` discovers exact package-level `NewX` functions associated with
local named `X`; a non-nil slice is a whitelist, including an empty whitelist.
There is no richest-constructor heuristic or `X{}` fallback. Accepted signatures
return `T` or `(T, error)`, with one optional leading exact `context.Context`.
Value and pointer keys stay distinct. Nongeneric wrappers may return closed
generic types; open generic functions, variadics and extra results fail.

Set `MatchIFoo` to opt into same-package `IFoo` → `Foo` matching, or use
`Config.Interfaces` for idiomatic interface names and exact implementations.
Structural implementation uniqueness is limited to the packages you supply.
Ignored and constructorless competitors still count; aliases and `Foo`/`*Foo`
count as one family. Ambiguity is an error, resolved only by an explicit mapping.
Interface-returning constructors bind their interface directly, not an inferred
concrete implementation. Self-bindings are owned; forwarders are borrowed and
inherit the concrete lifetime, so they share its cache and close it once.

Inspect `Plan.Diagnostics` using stable codes `BT001`–`BT013`, not message text.
Any error makes `Bindings` nil: never render a partial plan. Missing ordinary
dependencies produce informational `MissingDependency` obligations, or errors
with `RequireAllDependencies`. Scalar configuration requires an explicit provider
or `Existing` declaration and otherwise produces `MissingConfiguration`. The
container's `Build` still validates the final graph, cycles and captive lifetimes.
The [planner example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/bindingtypes/corpus_test.go) shows
type-checking and analysis without running application code.

Products render zero-argument constructor bindings with `di.Bind`. For one
through four ordered arguments, always use `BindFunc1` through `BindFunc4`,
adapting constructors without context or error results as needed. These typed
adapters prevent dependency-resolution failures from transferring ownership of
never-constructed zero values. There is no `BindFunc0`. For larger arities, use
`di.Bind`, resolve arguments in order and declare unique exact dependency keys.
Disposable value results with more than four arguments
are rejected until a safe runtime adapter exists: returning a zero value after
dependency failure could incorrectly transfer cleanup responsibility. Always
preserve a non-nil constructor result returned with an error. Render forwarders
with `di.BindBorrowed`, resolving their exact concrete key. The
[hand-authored render-equivalence tests](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/bindingtypes/render_test.go)
exercise caching, forwarding, close-once behavior and failed-result cleanup.

Duplicates remain strict. `KeepExisting` skips only keys explicitly listed in
`Config.Existing`, with their actual lifetimes: those bindings have action
`RetainExisting` and diagnostic `BT013`, and emit no registration. Multiple
generated candidates still fail. Preflight existing keys through `Catalog` when
available; otherwise your composition code must attest the supplied manifest.
Propagate every registration error—never catch `ErrDuplicate` and report success.
Applying unchanged generated wiring twice is not idempotent; refresh `Existing`
and replan to skip registered keys. If two product generators run in one app,
one owns the service registrations and the other lists those keys in `Existing`.

The checked-in [source corpus and expectation manifest](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/internal/bindingcorpus/cases.json)
is available to product renderer tests from their pinned Fundamentals module.
Consume fixture files, not the Go `internal` package. Each product must compile
and run its own output in an independent consumer module with `GOWORK=off` and
no sibling replacements; these shared planner tests do not certify a renderer.

## Choose lifetime and ownership separately

| Lifetime | Successful values | Owner of owned results |
| --- | --- | --- |
| `Singleton` | One per provider | Provider |
| `Scoped` | One per scope | Scope |
| `Transient` | New result per resolution | Scope, or provider when reached from a singleton |

Failed attempts are not cached. Current waiters share ordinary failure. A live
waiter retries an attempt canceled by its creator; canceling a waiter never
cancels the creator's work. Cancellation or deadline errors from failed-value
cleanup alone do not trigger a waiter retry, even when a dependency propagates
them. Factory and cleanup causes remain inspectable with `errors.Is` and
`errors.As`. A later explicit resolution can try again.

| Helper | Ownership |
| --- | --- |
| `Bind`, `BindFunc1..4` | Container disposes non-nil results, including failed results |
| `BindBorrowed`, `BindBorrowedFunc1..4` | Container never disposes results, even on failure or cancellation |
| `BindValue` | Externally owned borrowed singleton, validated immediately |

Borrowing a result does not borrow its dependencies: each dependency retains
its own binding's ownership. To forward an interface to an owned concrete
instance, borrow the interface binding and preserve the concrete lifetime:

```go
// Excerpt: registry is your container.Registry; Store is implemented by *SQLStore.
err := di.BindBorrowedFunc1(&registry, di.Scoped,
    func(_ context.Context, store *SQLStore) (Store, error) {
        return store, nil
    })
```

Register `*SQLStore` as owned and handle `err`. Resolving either key shares the
scoped object, and it closes once. The
[interface forwarding example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/container/example_test.go)
demonstrates this identity and cleanup. Do not bind the same instance as owned
under two keys: there is no pointer-identity disposal deduplication. Transient
forwarding still creates a fresh concrete instance per resolution.

## Open a scope for the work it serves

Open operation scopes explicitly, pass them explicitly, and close them after
you stop and join the application work. There is no provider or resolver in
`context.Context` and no root `Resolve`.

Bind client-lifetime artifacts as `Singleton`. At client startup, open a
short-lived scope, resolve the Singleton-bound artifact, and close the scope
immediately. The instance and its root-owned dependencies remain usable until
`Provider.Close`; closing the startup scope does not dispose them. The
[client-lifetime Singleton example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/container/example_test.go)
shows this pattern. Your application coordinates client Close and Provider.Close
ordering; do not use a retained scope to prescribe that ordering.

For snapshotted definitions, use **open, prepare, close**: open a preparation
scope, copy the definition data you need, then close it. Do not retain scoped
services or their dependencies after preparation. `Provider.Close` also drains
any outstanding scopes, but it does not stop or join application client work.

Closing immediately stops new resolution admission and joins admitted
resolutions, not application handlers. Scope cleanup follows reverse creation
order; provider cleanup closes scopes in reverse opening order, then root
values. Cleanup prefers `interface{ Close(context.Context) error }` over
`io.Closer`, attempts every closer and joins errors and recovered panics.

Cancellation before cleanup starts leaves the owner closing; a later Close can
resume. Once cleanup starts, callbacks run synchronously with the supplied
context, so cancellation is cooperative, not forced termination. Completed
closes retain their result and do not repeat cleanup. A concurrent close waiter
can return its own cancellation while cleanup continues. Failed non-nil owned
factory results get a separate cooperative 30-second cleanup budget, without
inheriting creator cancellation. None of these closes commit operation effects.

## Inspect capabilities and scope identity

`Catalog` is optional on `Registrar` and `ScopeFactory`; scopes do not implement
it. Type-assert it when validating parameters or planning registrations. When
absent, treat a key as resolvable and fail at resolution instead of rejecting
startup. The default provider and registry implement Catalog.

Call `registry.BindScopeFactory()` explicitly if a constructed service needs to
open fresh scopes from its methods at operation time. The borrowed singleton facade implements `ScopeFactory`,
`Catalog` and `ScopeOwner`. It exposes neither `Close` nor `Resolve`, so injected
code cannot close the provider or accidentally resolve through an ambient scope.
This registration uses the ordinary duplicate checks.

Use the injected `di.ScopeFactory` only after construction, never inside a
factory callback. Opening a scope during construction bypasses cycle and
captive-lifetime validation and can deadlock when resolving the service's own
in-flight construction.

Before borrowing a scope, a framework adapter can require `ScopeOwner` and
check `Owns(scope)`. Nil, typed-nil, wrapped, unknown and foreign scopes return
false; reject them with `ErrInvalidScope` before resolution. Identity persists
after closure. `ContextChecker` is another optional capability on Scope; the
default container implements it and returns `ErrClosed` after closing starts.

## Guard operation metadata

Use `container.WithContextGuard` to capture immutable metadata and its presence
at `NewScope`, never at Build. Capture callbacks return a `di.ContextCheck`;
checks run before every scoped resolution, including cached/borrowed values and
calls through a factory's restricted resolver. Unrelated context values may
change. Callbacks run outside internal locks and must support concurrent calls.

Capture or check rejection yields `ErrContextMismatch` with its cause; a panic
yields `ErrCallbackPanicked`. A nil option or capture fails Build; a nil
successful check fails scope creation. Do not retain the context in a guard.

Singleton construction and its transitive transient dependencies receive
contexts with **all values hidden**, but cancellation and deadlines preserved.
That internal root path bypasses request guards; ordinary callers still pass
guard checks before singleton access. Frameworks remain responsible for their
own principal, tenant and operation policy. Guards cannot inspect hidden values
captured by a closure.

## Look up keys and errors

Keys identify exact Go types, distinguishing pointers, interfaces and generic
instantiations. Resolution never searches registrations for assignable types.
An interface result must implement its key; a concrete result must have its
exact dynamic type. No conversion occurs. Use `KeyOf(reflect.Type)` for validated
runtime parameters, including `reflect.Call` integration. Zero keys are invalid;
`Type` returns nil for zero, and `String` is diagnostic, not a service identifier.

Use `errors.Is` and `errors.As`. `*di.Error` exposes Operation, Key, a copied Path,
Kind, Cause and Panic. `Unwrap() []error` preserves category and cause. Error text
starts with `dependencyinjection:` and is diagnostic-only; it does not display
service values, callback causes or panic payloads.

| Category | Recovery |
| --- | --- |
| `ErrInvalidRegistration`, `ErrDuplicate`, `container.ErrFrozen` | Correct wiring; use a fresh registry after successful Build |
| `ErrMissing`, `ErrCycle`, `ErrCaptiveLifetime` | Correct the declared graph before Build succeeds |
| `ErrUndeclaredDependency` | Declare the direct edge or use a typed constructor helper |
| `ErrResolverExpired`, `ErrConcurrentFactoryUse` | Do not retain or overlap calls on a factory resolver |
| `ErrNilValue`, `ErrWrongType` | Return a non-nil result matching the exact key |
| `ErrFactoryFailed` | Inspect the preserved factory cause; later resolutions can retry |
| `ErrInvalidScope`, `ErrClosed` | Use a valid owned open scope |
| `ErrContextMismatch` | Use matching guarded metadata or open a new scope |
| `ErrCallbackPanicked` | Inspect Panic and repair the callback; cleanup continues |

Pre-canceled contexts, scope creation and waiting may return bare
`context.Canceled` or `context.DeadlineExceeded`. Factory cancellation and
cleanup errors remain inspectable even when wrapped or joined. Close does not
require matching operation metadata: you can supply a dedicated cleanup context.

## Conformance levels

If you write a provider adapter, run the reusable suites from
`github.com/cratis/fundamentals.go/dependencyinjection/ditest` in your tests.
This package imports `testing`: import it only from `_test.go` files, never
production packages. Translate every `ditest.Config` field into a fresh provider
and return build failures as errors, rather than calling `t.Fatal` in the builder.

- **Level 1 — `RunProvider`:** exact keys, result validation, lifetimes and caching,
  owned and borrowed disposal, cleanup order, close semantics, scope ownership,
  Catalog and the explicit scope-factory facade. No capability is skipped.
- **Level 2 — `RunDeclaredGraph`:** additionally checks missing edges, cycles and
  captive lifetimes at build, declared-edge enforcement, expiring and non-overlapping
  factory views, singleton context isolation and guards supplied in Config.
  Run both suites to claim both levels. The default container runs both.
- **Resolver-only — `RunResolver`:** exact lookup, `ErrMissing`, typed-helper
  interoperability and cancellation. Passing it certifies nothing about lifetimes,
  disposal, guards, dependency graphs or ownership. Fixtures are externally owned;
  register any adapter teardown with `t.Cleanup`.

The [adapter conformance example](https://github.com/Cratis/Fundamentals.Go/blob/main/dependencyinjection/ditest/suites_test.go)
shows how to translate configuration and run both provider suites. Opaque typed
bindings prevent incompatible factory results without unsafe code; successful
provider lookups check result types, while resolver fixtures explicitly exercise
nil, typed-nil and wrong-type results through `di.Resolve`.

For compiled ecosystem examples, see [Ecosystem recipes](/fundamentals/go/recipes/).
The samber/do and dig bridges qualify only for resolver-only borrowed-singleton
use. The Fx recipe hosts the Fundamentals provider; its provider-level result
does not establish native Fx operation-scope equivalence.

## Know the limits

This is not full Microsoft.Extensions.DependencyInjection compatibility.
Convention planning is available, but constructor code generation belongs to
products. There is no runtime assembly scanning, implicit constructor or method
invocation, named/keyed
services, open-generic activation, collection synthesis, last-registration-wins,
root resolution or injectable current `Provider`/`Resolver`. Factories in the
default container receive only a restricted resolver, limited to declared direct
edges; it rejects overlapping calls, expires on return and joins already-admitted
child resolution. Plain and custom resolvers need not implement that graph policy.

Keep constructors explicit when composition is simple. Neither the contracts
nor the container chooses an Arc operation or Chronicle delivery lifetime, and
neither imports those frameworks. Conformance tests qualify the shared contracts,
not an adapter's application-specific policies or ecosystem integration.
