50 lines
2.4 KiB
Markdown
50 lines
2.4 KiB
Markdown
|
|
# 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. **Wait** — `Run` 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`.
|