Files
launcher/docs/adr/ADR-001-three-phase-lifecycle.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

2.4 KiB

ADR-001: Three-Phase Lifecycle

Status: Accepted Date: 2026-03-18

Context

Application bootstrap has distinct concerns that must happen in a specific order: resource allocation (open database connections, bind ports), dependency wiring (connect components to each other after all are ready), and service activation (start accepting requests, launch background goroutines). Conflating these phases leads to ordering bugs: a component trying to use a dependency that has not been initialised yet, or a service starting to accept requests before the database connection pool is ready.

Decision

The Launcher orchestrates four sequential phases, implemented in Run():

  1. OnInit — called for all registered components in registration order. Opens connections, allocates resources. No component is started yet.
  2. BeforeStart — all Hook functions registered via BeforeStart(hooks ...Hook) are called in registration order, after all OnInit calls have succeeded. This is the dependency injection wiring phase: all components are initialised and can be queried, but none are serving yet.
  3. OnStart — called for all components in registration order. Starts goroutines, begins serving. If any OnStart fails, stopAll is called immediately and Run returns an error.
  4. WaitRun blocks on either an OS signal (SIGINT, SIGTERM) or a programmatic Shutdown() call.
  5. OnStop (shutdown)stopAll is called, running OnStop for all components in reverse registration order.

Each phase gate is explicit: if any step returns an error, the lifecycle halts and the error is returned to the caller. The caller (typically main) decides whether to call os.Exit(1).

Consequences

  • OnInit can safely assume nothing is running yet — safe to block on slow operations like initial DNS resolution or schema migration.
  • BeforeStart hooks see a fully initialised set of components, making it the correct place for wiring that requires cross-component knowledge.
  • OnStart can safely assume all dependencies are wired and all peers are initialised — it is safe to start serving or spawning goroutines.
  • The three-phase split eliminates an entire class of "component not ready" race conditions without requiring any synchronisation between components.
  • The Component interface requires all three methods to be implemented. Components with no meaningful action for a phase return nil.