Files
launcher/CLAUDE.md
Rene Nochebuena f2e3faa1d6 feat(launcher): initial stable release v0.9.0
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/
2026-03-18 23:49:12 +00:00

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 — OnInitBeforeStart 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. See docs/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.
  • BeforeStart hooks run after all inits, before all starts — the correct place for dependency injection wiring. See docs/adr/ADR-003-before-start-hooks.md.
  • No singletons: New(logger, opts...) returns a Launcher interface; there is no package-level instance, no sync.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 OnInit or OnStart. Use BeforeStart hooks; OnInit has no guarantee that other components are ready, and OnStart is too late (components may already be serving).
  • Do not create a package-level Launcher variable. New() is the only constructor; call it from main.
  • Do not call os.Exit inside a Component method. Return errors; let main decide whether to exit.
  • Do not register components after calling Run. Append and BeforeStart are not safe to call concurrently with Run.
  • Do not rely on ComponentStopTimeout as a substitute for proper OnStop implementation. A timed-out stop means resources may leak.

Testing Notes

  • compliance_test.go asserts at compile time that New(logz.New(Options{})) returns a value satisfying launcher.Launcher.
  • launcher_test.go covers the full lifecycle: successful run + programmatic shutdown, OnInit failure aborting before OnStart, OnStart failure triggering stopAll, BeforeStart hook failure, reverse-order shutdown verification, and per-component stop timeout.
  • Tests use a lightweight mockComponent struct implementing Component with controllable error injection and call-order recording.
  • Run with plain go test — no external dependencies.