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 Configurer interface {
    Plugin
    Configure(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) Configure(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. Configure     (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 shutdown

Phase details

Configure — 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 configure:

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/users

The 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 root

Module 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.configure Plugin configure failed
app.register DI registration failed
app.start Plugin start failed
app.stop Plugin stop failed
app.invoke Invoke function failed
app.runner Custom runner failed