launcher only imports logz (Tier 1) — it belongs at Tier 2, not 5. The wrong tier implied it had to be pushed last, when in reality worker, postgres, mysql, sqlite, valkey, firebase, and httpserver all depend on it and cannot be tagged before it.
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: 2 (depends on Tier 1 logz)
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.