-
Notifications
You must be signed in to change notification settings - Fork 0
Dependency Injection
rex embeds a dix container and creates
one scope per request.
app.Container().Singleton(func() *Database {
return &Database{DSN: os.Getenv("DSN")}
})
func listUsers(ctx route.Context) {
var repo *UserRepo
if err := ctx.Resolver().Resolve(&repo); err != nil {
rextension.WriteProblem(ctx.ResponseWriter(), ctx.Request(),
500, rextension.ProblemInternal, "the request could not be completed")
return
}
_ = ctx.JSON(200, repo.FindAll(ctx))
}ctx.Resolver() is the request scope. Anything registered Scoped is built
at most once per request and closed when the handler returns.
| Built | Closed by the framework | |
|---|---|---|
Singleton |
once, lazily, shared | no — close it in your own shutdown |
Scoped |
once per request |
yes, if it has Close() error
|
Transient |
every resolve | no |
Instance |
by you | no |
Full detail: dix → Lifetimes.
A factory takes either nothing or a single Resolver. For a Scoped
registration, the resolver handed in is the scope:
app.Container().Scoped(func(r rextension.Resolver) *UnitOfWork {
var db *Database
if err := r.Resolve(&db); err != nil {
panic(err)
}
return db.Begin()
})Closing over app.Container() instead would resolve from the root, and any
scoped dependency would fail with dix.ErrScopedFromRoot. The captured form
works right up until one of its dependencies becomes scoped, and then fails
everywhere at once.
The single most useful pattern the request scope enables: make the safe outcome the default one.
type Tx struct {
tx *sql.Tx
committed bool
}
func (t *Tx) Commit() error { t.committed = true; return t.tx.Commit() }
func (t *Tx) Close() error { // called when the request ends
if t.committed {
return nil
}
return t.tx.Rollback()
}
app.Container().Scoped(func(r rextension.Resolver) *Tx { … })A handler that returns early — a validation failure, a recovered panic — rolls back without anyone remembering to.
The container may be written to at any time, but in practice do it before Run
or from an extension's OnInitialize. A registration made after the application
is serving is invisible to any resolve that already happened.
Registering the same type twice returns dix.ErrAlreadyRegistered. To replace
something — a default an extension installed, say — unbind first:
_, _ = app.Container().Unbind(oldLogger)
_ = app.Container().Instance(newLogger)Unbind keys on the exact dynamic type of the value passed, does not close
what it removes, and does not affect singletons already handed out.
app.Container() returns rextension.Container, not dix.Container. That is
why extensions do not need dix in their go.mod, and it means the container
implementation is swappable without touching extension code.
Application code may import dix — for the sentinel errors, or to build a
container directly in a test — but it does not have to.
Resolution errors otherwise surface on the first request that needs the missing piece. Move that to boot:
func verify(c rextension.Container) error {
// Use a scope, or every scoped dependency reports ErrScopedFromRoot.
type scoper interface{ NewScope() dix.Scope }
s := c.(scoper).NewScope()
defer s.Close()
var users *UserService
return s.Resolve(&users)
}Or, more simply, keep the wiring in one function and make it return an error:
if err := errors.Join(
app.Container().Instance(cfg),
app.Container().Singleton(newUserRepo),
app.Container().Scoped(newUnitOfWork),
); err != nil {
return fmt.Errorf("wiring: %w", err)
}A singleton holding a scoped value. It outlives every scope, so it pins the first request's transaction forever. Resolve the scoped value at the point of use.
Registering an interface type. Singleton(func() Logger { … }) makes
Logger the registered identity. Register the concrete type and let interface
matching find it.
Two implementations of one interface. Resolve then fails with
ErrAmbiguousResolution rather than picking one at random — Go randomises map
iteration, so "first match" would differ between process starts. Register one,
or use ResolveAll.
rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem