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/
44 lines
2.1 KiB
Markdown
44 lines
2.1 KiB
Markdown
# 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`, keeping `main` as the composition root without exposing internals.
|
|
- All `OnInit` guarantees (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 `OnStop` will still run for any
|
|
components that successfully ran `OnInit`, consistent with ADR-001's behaviour
|
|
when `OnStart` fails).
|
|
- The pattern scales to any number of wiring steps without adding methods to the
|
|
`Component` interface.
|