Service providers
This guide covers service providers in Tinka. You will learn how to:
- Write a provider that registers a service and releases it on shutdown
- Use each of the five lifecycle hooks
- Add providers to an App, and know the order their hooks run in
Overview
A service provider packages a service with its place in the application lifecycle: how the service is registered in the container, how it's configured, and how it's released when the App stops. A database package ships a provider that opens a connection pool and closes it on shutdown; a queue package ships one that starts its workers once the App is ready.
A provider is any Go value. It implements the hooks it needs, out of five, and the App calls them at the right time. Configuration is ordinary struct fields, set where the provider is created.
Write a provider
This provider registers a *sql.DB connection pool, checks the database is reachable at boot, and closes the pool on shutdown:
package main
import (
"context"
"database/sql"
"github.com/mucolabs/tinka"
"github.com/mucolabs/tinka/di"
)
type DatabaseProvider struct {
URL string
}
func (p *DatabaseProvider) Register(c *di.Container) {
c.Singleton(func(*di.Resolver) (*sql.DB, error) {
return sql.Open("pgx", p.URL)
})
}
func (p *DatabaseProvider) Boot(ctx context.Context, app *tinka.App) error {
db, err := app.Container().Make[*sql.DB]()
if err != nil {
return err
}
return db.PingContext(ctx)
}Add it to the App with Provide, before the App boots:
app.Provide(&DatabaseProvider{URL: e.String("DATABASE_URL")})The pool needs no Shutdown hook: the container closes every singleton it built that has a Close method, such as *sql.DB, when the App shuts down.
The lifecycle hooks
Each hook is a method with a fixed signature, and every one is optional:
A hook that returns an error stops the App from starting, and the App shuts down what it set up so far. A provider can also add App hooks from its own hooks, such as app.OnShutdown, for cleanup that depends on what it started.
Execution order
The App runs one phase at a time: Register for every provider, then Boot for every provider, and so on. Within a phase, providers run in the order they were added, except Shutdown, which runs in reverse, so a provider shuts down before the providers it depends on.
app.Provide(
&DatabaseProvider{URL: e.String("DATABASE_URL")}, // starts first, shuts down last
&QueueProvider{}, // uses the database
)Within each phase, the App's own hooks (OnBoot, OnStart, OnReady) run after every provider's, so application code can rely on what providers set up. OnShutdown hooks run before providers shut down, while their services still work.
When to write a provider
Write a provider when a service needs a place in the lifecycle: registration in the container, a check at boot, workers that start when the App is ready, or resources to release on shutdown. A package that integrates with Tinka ships one, so an App adds it with one line.
For a service that needs none of that, a constructor and a container binding in main are enough.
See also: Application lifecycle, Dependency injection