# Dependency injection

> Build and share an App's services with the di container: typed bindings, lifetimes, request scope, validation at boot and swaps in tests.

This guide covers dependency injection and the container in Tinka. You will learn:

- How to register a service with a typed factory and resolve it
- The four kinds of binding and how long each value lives
- How to bind interfaces and choose an implementation
- How to check every binding before the App serves anything
- How to swap a service for a fake in tests
- How services are released when the App stops

## Overview

Dependency injection means a piece of code receives what it depends on instead of building it. In Go you do this with constructor parameters: `posts.NewService(db, logger)` receives its database and its logger, so a test can pass others.

As an App grows, someone has to call all those constructors in the right order, decide which values are shared, and replace some of them in tests. The `di` package (`github.com/mucolabs/tinka/di`) is that someone: a **container** that knows how to build each service of the App, called a **binding**, and how long each built value lives.

A binding is a typed factory that asks for its own dependencies. There's no reflection and nothing to annotate: the compiler checks the wiring, and you can read it in the code.

## Your first binding

::::steps

:::step{title="Create the service"}

An `AvatarService` builds Gravatar URLs for users. It's plain Go: it knows nothing about the container.

```go title="avatars.go"
package main

import (
	"crypto/md5"
	"encoding/hex"
	"strings"
)

type AvatarService struct {
	Size string
}

func (s *AvatarService) URL(email string) string {
	sum := md5.Sum([]byte(strings.ToLower(email)))
	return "https://gravatar.com/avatar/" + hex.EncodeToString(sum[:]) + "?size=" + s.Size
}
```

:::

:::step{title="Register it in the container"}

Register a factory for `*AvatarService`. The factory's return type is the binding's key, and `Singleton` builds one value for the whole App. `Validate` builds every singleton now, so a mistake stops the App at boot, and `MustMake` returns the service:

```go title="main.go"
package main

import (
	"fmt"
	"log"

	"github.com/mucolabs/tinka/di"
)

func main() {
	c := di.New()
	c.Singleton(func(r *di.Resolver) (*AvatarService, error) {
		return &AvatarService{Size: "200"}, nil
	})
	if err := c.Validate(); err != nil {
		log.Fatal(err)
	}

	avatars := c.MustMake[*AvatarService]()
	fmt.Println(avatars.URL("ada@example.com"))
}
```

:::

:::step{title="Use it in a handler"}

Resolve services once, at boot, and capture them in your handlers, so a request pays nothing for the container:

```go title="main.go" ins={2-5}
	avatars := c.MustMake[*AvatarService]()
	app := tinka.New(tinka.HTTPConfig{})
	app.Get("/avatar", func(c *tinka.Ctx) (map[string]string, error) {
		return map[string]string{"url": avatars.URL(c.Query("email"))}, nil
	})
```

:::

::::

## Depending on other bindings

A factory receives a `*di.Resolver` and asks it for what the service needs. `MustMake` keeps the factory short: when a dependency is missing, its panic becomes the factory's error, so it reaches `Validate` or `Make` as an ordinary error:

```go
c.Value(dbConfig)
c.Singleton(func(r *di.Resolver) (*sql.DB, error) {
	return sql.Open("pgx", r.MustMake[DatabaseConfig]().URL)
})
c.Singleton(func(r *di.Resolver) (*posts.Service, error) {
	return posts.NewService(r.MustMake[*sql.DB](), r.MustMake[*slog.Logger]()), nil
})
```

Use `Make`, which returns an error, when the factory wants to handle a missing dependency itself.

## What can be bound

Any Go type can be a binding: a pointer to a struct, a struct, an interface or a configuration value. The key is the exact type, so `*DB` and `DB` are two bindings, and a factory that returns `Cache`, an interface, registers `Cache`. Because the type is the name, Tinka has no string aliases: there's nothing to misspell.

## Bindings

The kind of binding decides how long a built value lives.

::::options

:::option{name="Bind" type="func(f di.Factory[T])"}
Builds a new value every time one is made.
:::

:::option{name="Singleton" type="func(f di.Factory[T])"}
Builds one value the first time it's made, and shares it. A singleton is always built outside any request, so it can't keep one request's data. A factory that fails keeps failing with the same error until the App restarts.
:::

:::option{name="Scoped" type="func(f di.Factory[T])"}
Builds one value per resolver, which means one per request. See [during requests](#dependency-injection-during-requests).
:::

:::option{name="Value" type="func(v T)"}
Registers a value built elsewhere, such as configuration parsed at boot. Every `Make` returns it.
:::

::::

Registering the same type twice panics when the App boots, because the second registration would silently replace the first. Tests replace bindings with [`Swap`](#swapping-dependencies-in-tests).

## Dependency injection during requests

A `*di.Resolver` makes values for one request. `NewResolver` creates one with its own values and its own scoped values, and `Value` gives it something that exists only for that request, such as the signed-in user. A `Scoped` binding is built once per resolver and can use those values:

```go
c.Scoped(func(r *di.Resolver) (*Cart, error) {
	return carts.For(r.MustMake[User]()), nil
})

res := c.NewResolver()
res.Value(user)
cart := res.MustMake[*Cart]() // built for this request, then reused within it
```

A singleton can't depend on a scoped binding or on a request's value: it would keep the first request's value forever and serve it to every other user. Making one returns `di.ErrScoped`, and `Validate` reports it at boot.

The App creates these resolvers for you. Give it the container in `HTTPConfig`, and `c.Make` resolves through a resolver made for the request on first use. That resolver holds the request's `*tinka.Ctx`, so a scoped binding can read the request:

```go
c.Scoped(func(r *di.Resolver) (*Cart, error) {
	ctx := r.MustMake[*tinka.Ctx]()
	return carts.For(ctx.Query("session")), nil
})

app := tinka.New(tinka.HTTPConfig{Container: c})
app.Get("/cart", func(ctx *tinka.Ctx) (*Cart, error) {
	return ctx.Make[*Cart]()
})
```

A request that never calls `Make` pays nothing for the container.

## Interfaces as contracts

A service that depends on an interface works with any implementation, and the binding decides which one. Here business code depends on `Payments`, and one line in the wiring picks Stripe:

```go
type Payments interface {
	Charge(ctx context.Context, cents int64) error
}

c.Singleton(func(r *di.Resolver) (Payments, error) {
	return stripe.New(r.MustMake[StripeConfig]()), nil
})
```

To switch to another provider, change the factory. Code that calls `MustMake[Payments]()` doesn't change.

## Choosing an implementation

A factory is Go code, so it chooses an implementation with an `if`:

```go
c.Singleton(func(r *di.Resolver) (Search, error) {
	if r.MustMake[SearchConfig]().Driver == "typesense" {
		return typesense.New(), nil
	}
	return postgres.NewSearch(r.MustMake[*sql.DB]()), nil
})
```

When two services need different implementations of one interface, give each its own factory and pass the implementation it needs to its constructor.

## Resolution priority

`Make` looks in this order and uses the first match:

| Priority | Source | Registered with |
|---|---|---|
| 1 | Swaps | `c.Swap` in tests |
| 2 | Resolver values | `res.Value` during a request |
| 3 | The type's binding | `Bind`, `Singleton`, `Scoped` or `Value` |

A type with none of these fails with `di.ErrNotRegistered`, and an error message that names the chain of types being built:

```text title="Output"
di: cannot make *posts.Service: *sql.DB is not registered (*posts.Service → *sql.DB)
```

## Validating bindings at boot

`Validate` builds every singleton and returns every problem at once: a missing binding, a dependency cycle, a singleton that depends on request data, or a factory's own error. Call it after registering and before serving, so a misconfigured App stops at boot instead of failing on its first request. `Bind` and `Scoped` factories run only when made, because they may need a request's values.

## Swapping dependencies in tests

`Swap` replaces a binding with a fake, everywhere and in every resolver, until `Restore`. Register the restore with `t.Cleanup` so it runs even when the test fails:

```go title="checkout_test.go"
func TestCheckout(t *testing.T) {
	c := newContainer()
	c.Swap(func(*di.Resolver) (Payments, error) {
		return &fakePayments{}, nil
	})
	t.Cleanup(func() { c.Restore[Payments]() })

	// Code that makes Payments now receives the fake.
}
```

A swap builds a new value on every `Make`.

## Reacting to resolved bindings

`OnResolve` runs a function on every value the container builds of a type: once for a singleton, once per resolver for a scoped binding, and every time for `Bind`. Use it to configure what a package builds, or to log it. An error from the function fails the `Make`:

```go
c.OnResolve(func(db *sql.DB, _ *di.Resolver) error {
	db.SetMaxOpenConns(20)
	return nil
})
```

## Shutting down

`Shutdown` releases the singletons the container built, in the reverse of the order they were built, so a service shuts down before what it depends on. An App calls it for you, as the last step of its [shutdown](application-lifecycle.md#the-shutdown-phase). It calls `Shutdown(ctx)` on a `di.Shutdowner` and `Close` on an `io.Closer`, such as `*sql.DB`:

```go
if err := c.Shutdown(ctx); err != nil {
	logger.Error("shutdown failed", "err", err)
}
```

See also: [HTTP context](../basics/http-context.md), [Configuration & Environment](../../configuration.md), [Logger](../digging-deeper/logger.md), [Routing](../basics/routing.md)
