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/
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():
- OnInit — called for all registered components in registration order. Opens connections, allocates resources. No component is started yet.
- BeforeStart — all
Hookfunctions registered viaBeforeStart(hooks ...Hook)are called in registration order, after allOnInitcalls have succeeded. This is the dependency injection wiring phase: all components are initialised and can be queried, but none are serving yet. - OnStart — called for all components in registration order. Starts goroutines,
begins serving. If any
OnStartfails,stopAllis called immediately andRunreturns an error. - Wait —
Runblocks on either an OS signal (SIGINT,SIGTERM) or a programmaticShutdown()call. - OnStop (shutdown) —
stopAllis called, runningOnStopfor 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
OnInitcan safely assume nothing is running yet — safe to block on slow operations like initial DNS resolution or schema migration.BeforeStarthooks see a fully initialised set of components, making it the correct place for wiring that requires cross-component knowledge.OnStartcan 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
Componentinterface requires all three methods to be implemented. Components with no meaningful action for a phase returnnil.