tinka alpha

Service providers

This guide covers service providers in Tinka. You will learn how to:

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:

database.go
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:

main.go
	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:

Registerfunc(c *di.Container)

Binds the provider's services in the container. It runs first, for every provider, before any other hook. It must not resolve services: providers added later haven't registered theirs yet.

Bootfunc(ctx context.Context, app *tinka.App) error

Configures services, now that every provider registered: resolve what you need, and check it works, such as pinging a database.

Startfunc(ctx context.Context, app *tinka.App) error

Assembles the App, before its start hooks register routes: register middleware, or routes the package serves itself.

Readyfunc(ctx context.Context, app *tinka.App) error

Runs once the App serves requests: start workers and other long-running work.

Shutdownfunc(ctx context.Context, app *tinka.App) error

Releases what the provider opened itself, such as the workers it started in Ready, before the deadline in ctx.

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.

main.go
	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