Plugins & Lifecycle
go.putnami.dev/app provides the application lifecycle, plugin architecture, and module composition system.
Application
The application is the root module and lifecycle orchestrator:
import "go.putnami.dev/app"
a := app.New("my-service")
a.Module.Use(httpServer)
a.Module.Use(healthPlugin)
a.ListenAndServe()Application methods
| Method | Description |
|---|---|
New(name) |
Create a new application |
ProvideFunc(constructors...) |
Register constructor-based DI providers |
ProvideScopedFunc(constructors...) |
Register scoped constructor providers |
InvokeFunc(fns...) |
Run functions after DI is built |
Run(fn) |
Set a custom runner |
Start(ctx) |
Start the application lifecycle |
Stop() |
Graceful shutdown |
ListenAndServe() |
Start + signal handling + Stop |
Context() |
Get the DI ContainerContext |
IsRunning() |
Check if running |
ListenAndServe
ListenAndServe() is the standard entry point. It calls Start(), waits for SIGINT or SIGTERM, then calls Stop():
func main() {
a := app.New("my-service")
a.Module.Use(server)
if err := a.ListenAndServe(); err != nil {
log.Fatal(err)
}
}Custom runner
For applications that need a main loop:
a.Run(func(ctx context.Context) error {
// ctx is canceled on shutdown
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
processJobs()
}
}
})Plugins
Every framework component (HTTP server, SQL pool, event broker, health checks) is a plugin. Plugins implement lifecycle interfaces.
Plugin interface
The base interface requires only a name:
type Plugin interface {
Name() string
}Lifecycle interfaces
Implement one or more to participate in the application lifecycle:
// Sequential — initialize resources, register routes
type Warmer interface {
Plugin
Warmup(owner *Module) error
}
// Parallel — start servers, subscribe to events
type Starter interface {
Plugin
Start(owner *Module) error
}
// Reverse order — graceful shutdown
type Stopper interface {
Plugin
Stop(owner *Module) error
}
// Build-time — code generation
type Generator interface {
Plugin
Generate(owner *Module) error
}Creating a plugin
type MetricsPlugin struct {
collector *MetricsCollector
}
func NewMetricsPlugin() *MetricsPlugin {
return &MetricsPlugin{}
}
func (p *MetricsPlugin) Name() string { return "metrics" }
func (p *MetricsPlugin) Warmup(owner *app.Module) error {
p.collector = NewMetricsCollector()
return nil
}
func (p *MetricsPlugin) Start(owner *app.Module) error {
return p.collector.Start()
}
func (p *MetricsPlugin) Stop(owner *app.Module) error {
return p.collector.Flush()
}Registering plugins
a := app.New("my-service")
a.Module.Use(fhttp.NewServerPlugin(fhttp.ServerConfig{Port: 3000}))
a.Module.Use(fhttp.NewHealthPlugin())
a.Module.Use(NewMetricsPlugin())Lifecycle phases
The application lifecycle runs in this order:
1. Warmup (sequential) — plugins prepare resources, register routes
2. Build DI (if providers) — validate graph, resolve singletons
3. Invoke (sequential) — run InvokeFunc functions
4. Start (parallel) — plugins start servers
5. Run (if set) — execute custom runner
6. [wait for signal]
7. Stop (reverse) — graceful shutdownPhase details
Warmup — Plugins initialize in registration order. This is where the HTTP server registers routes from the module path prefix, and plugins prepare any resources they need.
Build DI — Only runs if ProvideFunc, ProvideScopedFunc, or Module.Provide were called. The container validates the dependency graph and resolves all non-lazy singletons.
Invoke — Functions registered with InvokeFunc run with DI-resolved parameters. Use this to wire routes or perform setup that requires resolved dependencies.
Start — All Starter plugins start in parallel. The HTTP server begins listening, event brokers start processing, etc.
Stop — On shutdown, Stopper plugins stop in reverse registration order. Shutdown hooks registered with OnStop also run. onClose hooks from DI providers run last.
Modules
Modules group plugins, DI providers, and sub-modules into composable units:
// Create a module
auth := app.NewModule("auth")
auth.Path("/auth")
auth.Provide(inject.AutoProvide(NewAuthService))
auth.Use(authPlugin)
// Create another module
api := app.NewModule("api")
api.Path("/api")
api.Use(apiPlugin)
// Compose into the application
a := app.New("my-service")
a.Module.Use(server)
a.Module.Use(auth)
a.Module.Use(api)Module path prefix
Modules can define a path prefix. The HTTP server prepends the full module path to all routes registered during warmup:
api := app.NewModule("api")
api.Path("/api/v1")
// Routes registered in this module's plugins will be prefixed with /api/v1
// e.g., GET /users → GET /api/v1/usersThe full path is computed from root to leaf:
root := app.NewModule("root")
root.Path("/app")
child := app.NewModule("child")
child.Path("/api")
child.FullPath() // "/app/api" when mounted under rootModule DI
Modules can register their own providers and declare requirements:
auth := app.NewModule("auth")
auth.Require(inject.TokenOf[*Database]()) // must exist in parent
auth.Provide(inject.AutoProvide(NewAuthService))Requirements are validated during the DI build phase. If a required token is not provided by the parent or root, startup fails with a requirement_not_met error.
Module security
Apply default security options to all endpoints in a module:
admin := app.NewModule("admin")
admin.Path("/admin")
admin.Secure(&app.SecurityOptions{
Roles: []string{"admin"},
})See Security for details.
Shutdown hooks
a.Module.OnStop(func() error {
fmt.Println("cleaning up...")
return nil
})Introspection
// Collect all plugins from the module tree
plugins := a.Module.CollectPlugins()
// Collect all modules (self + descendants)
modules := a.Module.CollectModules()
// Collect all shutdown hooks
hooks := a.Module.CollectShutdownHooks()Constructor-based DI
The application provides a convenient fx-style DI API:
a := app.New("my-service")
// Register constructors
a.ProvideFunc(
NewDatabase, // func(cfg *Config) (*Database, error)
NewUserService, // func(db *Database) *UserService
)
// Scoped constructors
a.ProvideScopedFunc(
NewRequestContext, // func() *RequestContext — new per scope
)
// Invoke after DI is built
a.InvokeFunc(func(users *UserService, server *fhttp.ServerPlugin) {
server.GET("/users", func(ctx *fhttp.Context) *fhttp.Response {
return fhttp.JSON(users.List(ctx.Context()))
})
})DI is optional. Applications with no registered providers skip container creation entirely.
Error codes
| Code | Description |
|---|---|
app.already_running |
Application is already running |
app.warmup |
Plugin warmup failed |
app.register |
DI registration failed |
app.start |
Plugin start failed |
app.stop |
Plugin stop failed |
app.shutdown |
Shutdown error |
app.invoke |
Invoke function failed |
app.runner |
Custom runner failed |
Related guides
- Dependency Injection — DI container details
- HTTP & Middleware — HTTP server plugin
- Configuration — config loading