Skip to content

Service Container

Your handler needs to send an email. The mail transport was built at startup, in a completely different file. How does your handler get hold of it?

You could pass it down through every function between the two, which means every function in the middle takes an argument it does not use. Or you could build a new one inside the handler, which means a new connection per request and no way to swap it for a fake in a test.

The service container is the third option: a box you put ready-made things into at startup and take them out of wherever you need them, keyed by their Go type.

// at startup, in a provider
app.AddService(mailer)

// later, in a handler
mailer := app.Get[*mail.Mailer](a)

Because the key is the type, you get a *mail.Mailer back — no casting, and a typo is a compile error.

Adding services

Register services via app.AddService():

type MyService struct {
    // ...
}

func (p *MyProvider) Provide(a app.App) error {
    svc := &MyService{}
    a.AddService(svc)
    return nil
}

Retrieving Services

Via the App Instance

svc := a.Service(&MyService{}).(*MyService)

Via the Generic Helper

import "github.com/lemmego/api/app"

svc := app.Get[*MyService](a)

The generic Get[T] helper composes with type safety:

session := app.Get[*session.Session](a)
filesystem := app.Get[*fs.FileSystem](a)

Built-in Services

The framework registers several services through its providers:

ServiceTypeProvider
Session Manager*session.Sessionsession.Provider
File System*fs.FileSystemfs.Provider
Database*orm.DBormconnector
Database*gorm.DB / *bun.DBgormconnector / bunconnector
Inertia*inertia.Inertiainertia.Provider
Auth*auth.Authauth.Provider

How It Works

The container uses reflection to store services by their pointer type:

type serviceContainer struct {
    services map[reflect.Type]interface{}
    mutex    sync.RWMutex
}

func (sc *serviceContainer) Add(service interface{}) {
    t := reflect.TypeOf(service)
    sc.services[t] = service
}

func (sc *serviceContainer) Get(service interface{}) error {
    // Resolves the service by type and populates the provided pointer
}

When to Use

Use the service container for:

  • Framework internals (session, filesystem, database connections)
  • Simple service registration where you need exactly one instance
  • Services that need to be available across the entire application

For more advanced DI needs (multiple lifetimes, auto-resolution of dependencies, scoped instances), see the Dependency Injection page.