Decisions
Application lifecycle
Status: Accepted (2026-09-14) · Supersedes (with ADR-0020): ADR-0004
Context#
The earlier Starter/Stopper interfaces were ambiguous (is Start blocking?), didn't handle startup failure, had no readiness drain, and didn't fit background workers. Apps include an HTTP server, job workers, a database pool and telemetry exporters that must start and stop in a correct order.
Options#
Starter/Stopperinterfaces with ordered lists.- A DI container with lifecycle hooks (Fx).
- Constructors for setup, one
Runnerinterface for long-running work,io.Closerfor resources, and a small run helper that owns the shutdown sequence.
Decision#
Option 3.
| Concern | Design |
|---|---|
| Setup | Constructors do blocking initialisation with a context timeout (open pool, ping, load keys) and return (*T, error). Order is the construction order in internal/app, visible and compile-checked. |
| Resources | Implement the standard io.Closer. |
| Long-running work | type Runner interface { Run(ctx context.Context) error }. Run blocks until ctx is done and returns nil on graceful exit. Examples: HTTP server, job workers, schedulers. |
| Startup failure | A cleanup stack: every successfully constructed resource is registered; if a later constructor fails, registered resources close in reverse order and the process exits non-zero. |
| Running | Runners run under errgroup; the first unexpected error cancels the others. |
| Shutdown | 1. Signal received → /readyz returns 503. 2. Drain wait (default 5s) for load balancers. 3. Cancel the runner context; HTTP server shuts down gracefully, workers stop taking jobs. 4. Hard deadline (default 25s). 5. Close resources in reverse order. 6. Flush telemetry last. 7. A second signal forces exit. |
| Telemetry | Constructed first, flushed last. |
| Migrations | Never run implicitly at startup. Separate migrate command; optional startup flag, off by default in production. |
Core package apistock.dev/app provides Runner, the cleanup stack and the run helper (stdlib + x/sync/errgroup only).
Why#
- Correct behaviour on deploys: no dropped requests, no leaked goroutines, no lost telemetry.
- No container, no reflection; the lifecycle reads top to bottom in
internal/app/app.go.
Trade-offs#
- Drain and deadline defaults must be tuned per platform (documented, configurable).
- Developers write construction order by hand (the generator writes the initial version).
Consequences#
- Every official module states whether its types are
Runner,io.Closer, both or neither. - Jobs and HTTP must respect context cancellation; tests cover graceful shutdown.
Was this page useful?Open an issue