Skip to content

Dependency Injection

Lakta uses github.com/samber/do/v2 for dependency injection. The injector lives on the context, so it flows naturally through the module lifecycle and into request handlers.

func (m *MyModule) Init(ctx context.Context) error {
lakta.Provide(ctx, func(i do.Injector) (*MyService, error) {
return &MyService{}, nil
})
return nil
}

Providers are lazy — they are only constructed when first invoked.

The idiomatic way to consume DI in gRPC and HTTP handlers is lakta.Invoke[T], which takes the request context directly:

// gRPC handler
func (s *MyServer) GetThing(ctx context.Context, req *pb.GetThingRequest) (*pb.GetThingResponse, error) {
svc, err := lakta.Invoke[*MyService](ctx)
if err != nil {
return nil, err
}
return svc.GetThing(ctx, req.Id)
}
// HTTP handler (Fiber)
func handleGet(c fiber.Ctx) error {
svc, err := lakta.Invoke[*MyService](c.Context())
if err != nil {
return err
}
return c.JSON(svc.Get())
}

When wiring modules together in Init, retrieve the injector explicitly:

func (m *MyModule) Init(ctx context.Context) error {
injector := lakta.GetInjector(ctx)
dep := do.MustInvoke[*Dependency](injector)
m.svc = NewMyService(dep)
return nil
}

Modules like grpc/client register a typed client directly when you provide a constructor via WithClient. This means you invoke the interface type, not the raw connection:

// Registration (in NewRuntime)
grpcclient.NewModule(
grpcclient.WithName("data"),
grpcclient.WithClient(v1.NewDataServiceClient),
)
// Usage in any handler
client, err := lakta.Invoke[v1.DataServiceClient](ctx)

Providers are scoped to the injector’s lifetime. When the runtime shuts down, do calls Shutdown on any provider that implements it, in reverse registration order.