Putnami
DocsGitHub

Licensed under FSL-1.1-MIT

Getting Started
Concepts
How To
Build A Web App
Build An Api Service
Share Code Between Projects
Configure Your App
Add Persistence
Add Authentication
Add Background Jobs
Principles
Tooling & Workspace
Workspace Overview
Cli
Jobs & Commands
SDK
Error Handling
Extensions
Typescript
Go
Python
Docker
Ci
Frameworks
Typescript
OverviewWebReact RoutingForms And ActionsStatic FilesApiErrors And ResponsesConfigurationLoggingHttp And MiddlewareDependency InjectionPlugins And LifecycleSessionsAuthPersistenceEventsStorageCachingWebsocketsTestingHealth ChecksTelemetryProto GrpcSmart Client
Go
OverviewHttpDependency InjectionPlugins And LifecycleConfigurationSecurityPersistenceErrorsEventsStorageCachingLoggingTelemetryGrpcService ClientsValidationOpenapiTesting
Platform
  1. DocsSeparator
  2. FrameworksSeparator
  3. GoSeparator
  4. Plugins And Lifecycle

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 shutdown

Phase 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/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.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

On this page

  • Plugins & Lifecycle
  • Application
  • Application methods
  • ListenAndServe
  • Custom runner
  • Plugins
  • Plugin interface
  • Lifecycle interfaces
  • Creating a plugin
  • Registering plugins
  • Lifecycle phases
  • Phase details
  • Modules
  • Module path prefix
  • Module DI
  • Module security
  • Shutdown hooks
  • Introspection
  • Constructor-based DI
  • Error codes
  • Related guides