Skip to content

Recipes

wiki edited this page Sep 4, 2026 · 1 revision

Recipes

Wire everything in one place, check every error

func wire(c dix.Container, cfg *Config) error {
	db, err := sql.Open("postgres", cfg.DSN)
	if err != nil {
		return fmt.Errorf("open db: %w", err) // failable work happens here
	}

	return errors.Join(
		c.Instance(cfg),
		c.Instance(&Database{Pool: db}),
		c.Singleton(newUserRepo),
		c.Scoped(newUnitOfWork),
		c.Transient(newIDGenerator),
	)
}

Request scope by hand

Outside rex, a scope per request looks like this:

func handler(c dix.Container) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		scope := c.NewScope()
		defer func() {
			if err := scope.Close(); err != nil {
				log.Printf("scope cleanup: %v", err)
			}
		}()

		var svc *UserService
		if err := scope.Resolve(&svc); err != nil {
			http.Error(w, "internal", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(svc.List(r.Context()))
	}
}

In rex this is already done for you — ctx.Resolver() is the request scope.

A transaction that rolls back unless committed

The cleanest use of scoped cleanup: 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() }

// Close runs when the scope ends.
func (t *Tx) Close() error {
	if t.committed {
		return nil
	}
	return t.tx.Rollback()
}

c.Scoped(func(r dix.Resolver) *Tx {
	var db *Database
	if err := r.Resolve(&db); err != nil {
		panic(err)
	}
	raw, err := db.Pool.Begin()
	if err != nil {
		panic(err)
	}
	return &Tx{tx: raw}
})

A handler that returns early — a validation failure, a panic recovered upstream — rolls back without anyone remembering to.

Collecting every implementation of an interface

type HealthCheck interface {
	Name() string
	Check(context.Context) error
}

c.Singleton(func() *DBCheck { return &DBCheck{} })
c.Singleton(func() *RedisCheck { return &RedisCheck{} })

var checks []HealthCheck
if err := c.ResolveAll(&checks); err != nil { ... }

ResolveAll wants a pointer to a slice of interfaces. Note this is the one place where several registrations satisfying one interface is intended rather than ambiguousResolve would reject exactly this set.

Distinguishing two values of the same type

type ReadDB struct{ *sql.DB }
type WriteDB struct{ *sql.DB }

c.Instance(&ReadDB{replica})
c.Instance(&WriteDB{primary})

Swapping a dependency for a test

func TestUserService(t *testing.T) {
	c := dix.New()
	c.Instance(&StubRepo{Users: []User{{ID: "1"}}}) // registers *StubRepo
	c.Singleton(newUserService)                     // resolves UserRepo iface

	var svc *UserService
	if err := c.Resolve(&svc); err != nil {
		t.Fatal(err)
	}
	// ...
}

Build a fresh container per test. There is no global state to reset, and a shared container across tests reintroduces exactly the cross-test leakage the package-level registries elsewhere in this ecosystem were removed to avoid.

Replacing a registration made by someone else

// An extension registered a default; the application wants its own.
if removed, err := c.Unbind(defaultLogger); err != nil {
	return err
} else if !removed {
	// nothing was registered under that concrete type — usually a sign the
	// default is a different type than you assumed
}
_ = c.Instance(myLogger)

Unbind keys on the exact dynamic type of the value passed, so pass a value of the type that was actually registered, not the interface you hold it in.

Verifying wiring at startup

Resolution errors otherwise surface on the first request that needs the missing piece. A smoke resolve in main moves that to boot:

func verify(c dix.Container) error {
	scope := c.NewScope()
	defer scope.Close()

	var (
		users  *UserService
		orders *OrderService
	)
	return errors.Join(
		scope.Resolve(&users),
		scope.Resolve(&orders),
	)
}

Use a scope, not the container, or every scoped dependency reports ErrScopedFromRoot and the check fails for the wrong reason.

Clone this wiki locally