Application lifecycle manager enforcing a three-phase init/wire/start sequence with reverse-order graceful shutdown and per-component stop timeouts. What's included: - `Component` interface (OnInit / OnStart / OnStop) and `Hook` type for BeforeStart wiring functions - `Launcher` interface with Append, BeforeStart, Run (blocks on SIGINT/SIGTERM), and idempotent Shutdown(ctx) - `New(logger, opts...)` constructor with configurable ComponentStopTimeout (default 15 s); no global state Tested-via: todo-api POC integration Reviewed-against: docs/adr/
4.0 KiB
launcher
Application lifecycle manager: init, wire, start, wait, and graceful shutdown.
Purpose
launcher orchestrates the startup and shutdown of all infrastructure components
(database pools, HTTP servers, background workers) in a Go service. It enforces a
strict phase order — OnInit → BeforeStart hooks → OnStart → wait for signal
→ OnStop in reverse — so that components are never started before their
dependencies are ready and never stopped before the components that depend on them.
Tier & Dependencies
Tier: 5 (application bootstrap only)
Imports: context, os, os/signal, sync, syscall, time (stdlib);
code.nochebuena.dev/go/logz (micro-lib)
Must NOT import: xerrors, rbac, or any domain/application module.
launcher is the composition root; it should not depend on what it orchestrates.
Key Design Decisions
- Three-phase lifecycle (
OnInit/BeforeStart/OnStart) with reverse-order shutdown. Seedocs/adr/ADR-001-three-phase-lifecycle.md. - Shutdown runs in reverse registration order; each component gets an independent
per-component timeout (default 15 s). See
docs/adr/ADR-002-reverse-order-shutdown.md. BeforeStarthooks run after all inits, before all starts — the correct place for dependency injection wiring. Seedocs/adr/ADR-003-before-start-hooks.md.- No singletons:
New(logger, opts...)returns aLauncherinterface; there is no package-level instance, nosync.Once, no global state. Shutdown(ctx)is idempotent — safe to call from multiple goroutines.
Patterns
Basic wiring:
logger := logz.New(logz.Options{JSON: true})
lc := launcher.New(logger)
lc.Append(db, cache, server)
lc.BeforeStart(func() error {
return server.RegisterRoutes(db, cache)
})
if err := lc.Run(); err != nil {
logger.Error("launcher failed", err)
os.Exit(1)
}
Custom component stop timeout:
lc := launcher.New(logger, launcher.Options{
ComponentStopTimeout: 30 * time.Second,
})
Implementing the Component interface:
type DBClient struct { pool *sql.DB }
func (d *DBClient) OnInit() error { d.pool, err = sql.Open(...); return err }
func (d *DBClient) OnStart() error { return nil } // no goroutines to start
func (d *DBClient) OnStop() error { return d.pool.Close() }
Programmatic shutdown (e.g. from a test):
go func() { lc.Run() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
lc.Shutdown(ctx)
Registration order matters:
// Register dependencies before dependents
lc.Append(db) // stopped last
lc.Append(cache)
lc.Append(server) // stopped first (reverse order)
What to Avoid
- Do not perform dependency injection wiring in
OnInitorOnStart. UseBeforeStarthooks;OnInithas no guarantee that other components are ready, andOnStartis too late (components may already be serving). - Do not create a package-level
Launchervariable.New()is the only constructor; call it frommain. - Do not call
os.Exitinside aComponentmethod. Return errors; letmaindecide whether to exit. - Do not register components after calling
Run.AppendandBeforeStartare not safe to call concurrently withRun. - Do not rely on
ComponentStopTimeoutas a substitute for properOnStopimplementation. A timed-out stop means resources may leak.
Testing Notes
compliance_test.goasserts at compile time thatNew(logz.New(Options{}))returns a value satisfyinglauncher.Launcher.launcher_test.gocovers the full lifecycle: successful run + programmatic shutdown,OnInitfailure aborting beforeOnStart,OnStartfailure triggeringstopAll,BeforeStarthook failure, reverse-order shutdown verification, and per-component stop timeout.- Tests use a lightweight
mockComponentstruct implementingComponentwith controllable error injection and call-order recording. - Run with plain
go test— no external dependencies.