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.
launcher
Application lifecycle manager for Go services.
| Module | code.nochebuena.dev/go/launcher |
| Tier | 2 — depends on logz |
| Go | 1.25 |
| Dependencies | code.nochebuena.dev/go/logz |
Overview
launcher orchestrates a repeatable lifecycle sequence — init, assemble, start, wait, shutdown — across any number of registered infrastructure components. It does not manage dependency injection, provide a service locator, or define what your components are; it only drives them through a consistent set of phases.
Installation
go get code.nochebuena.dev/go/launcher
Quick start
logger := logz.New(logz.Options{})
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)
}
Usage
Implementing Component
Any infrastructure piece implements the three-method interface:
type Component interface {
OnInit() error // open connections, allocate resources
OnStart() error // start goroutines, begin serving
OnStop() error // stop and release all resources
}
Lifecycle sequence
OnInit (all components, in registration order)
BeforeStart hooks (in registration order)
OnStart (all components, in registration order)
── application is running ──
OnStop (all components, in reverse registration order)
Run blocks at the "application is running" stage until SIGINT/SIGTERM is received or Shutdown is called. If any OnInit, hook, or OnStart returns an error, Run returns that error immediately (triggering stopAll for OnStart failures).
BeforeStart hooks
Hooks run after all OnInit calls and before any OnStart call. Use them to wire dependencies that require all components to be initialized first:
lc.BeforeStart(func() error {
// db and cache are both initialized at this point
return server.RegisterRoutes(db, cache)
})
Programmatic shutdown
Shutdown triggers graceful shutdown and waits for Run to return. Use it in tests or when your application needs to shut down without an OS signal:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := lc.Shutdown(ctx); err != nil {
// ctx expired before Run returned
}
Shutdown is idempotent — safe to call multiple times.
Customising the stop timeout
lc := launcher.New(logger, launcher.Options{
ComponentStopTimeout: 5 * time.Second,
})
The default is 15 seconds per component. The timeout controls how long stopAll waits for each individual OnStop call; it is independent of the ctx passed to Shutdown.
Design decisions
Run()returnserrorinstead of callingos.Exit. The original implementation calledos.Exit(1)directly, making the package untestable. Returning the error lets the caller (typicallymain) decide what to do.shutdownCh+doneChchannel pair.shutdownChis closed byShutdownto unblockRun's wait loop.doneChis closed byRun(viadefer) before it returns. This guarantees thatShutdownunblocks even whenRunreturns early due to an error inOnInitor a hook.- Components stopped in reverse registration order. This mirrors the LIFO teardown convention: the last component registered (typically the outermost layer, e.g. HTTP server) is stopped first, allowing inner layers (e.g. database) to remain available until no longer needed.
- Per-component timeout, not a global one. Each
OnStopgets its own independent timeout window. A single slow component does not consume the budget for all others.
Ecosystem
Tier 0 xerrors rbac
Tier 1 logz valid
Tier 2 ▶ launcher
Tier 3 postgres mysql sqlite valkey firebase httpclient worker
Tier 4 httputil httpmw httpauth-firebase httpserver
Tier 5 telemetry
launcher is the lifecycle backbone of the framework. Every Tier 3+ module implements launcher.Component; Run() drives them all through init → start → stop.
License
MIT