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.1 KiB
ADR-003: BeforeStart Hooks for Dependency Injection Wiring
Status: Accepted Date: 2026-03-18
Context
After all components have been initialised (OnInit), some wiring cannot be
expressed at construction time because it requires the initialised state of multiple
components simultaneously. For example, an HTTP server may need to register routes
that reference handler functions which themselves hold references to a database
client — but the database client only becomes ready after OnInit runs, which is
after all components are constructed.
One alternative is to wire everything in main before calling Run, but that
requires main to know the internal structure of every component, defeating
encapsulation. Another alternative is to wire in OnStart, but at that point other
components may already be running and the window for setup errors is narrower.
Decision
Launcher.BeforeStart(hooks ...Hook) registers functions of type func() error
that are called after all OnInit calls succeed and before any OnStart call
begins. Hooks are called in registration order. If any hook returns an error, Run
returns that error immediately without proceeding to OnStart.
Hook is a plain function type with no parameters beyond what closures capture.
This allows hooks to close over the components they wire, without launcher needing
to know anything about those components beyond the Component interface.
Consequences
- Dependency injection wiring is expressed as closures registered with
BeforeStart, keepingmainas the composition root without exposing internals. - All
OnInitguarantees (connections open, ports bound, resources allocated) are satisfied before any hook runs — hooks can safely call methods on initialised components. - Hook errors abort the lifecycle cleanly. No components have started yet, so no
cleanup of running services is needed (though
OnStopwill still run for any components that successfully ranOnInit, consistent with ADR-001's behaviour whenOnStartfails). - The pattern scales to any number of wiring steps without adding methods to the
Componentinterface.