Runtime Lifecycle
The runtime lifecycle is the ordered path from App construction to startup, command execution, runtime work, and graceful shutdown.
GoForj keeps lifecycle behavior explicit so commands, HTTP servers, workers, schedulers, metrics, events, and storage all have a predictable place to start and stop.
When To Use Lifecycle Hooks
| Question | Guidance |
|---|---|
| Use this when | App behavior must run at startup or shutdown with injected dependencies. |
| Avoid this when | The behavior belongs to a request, job, schedule, command, constructor, or package init function. |
| Start with | internal/app/lifecycle_registry.go and small service methods registered on the right phase. |
| Upgrade to | Runtime-specific lifecycle policy only when the behavior is framework-owned or belongs to every generated App. |
Why It Exists
Go applications often accumulate startup behavior in main.go, package init functions, or hidden global state.
GoForj avoids that. The generated App creates dependencies through Wire, registers lifecycle hooks explicitly, starts the App before command execution, and shuts it down through the lifecycle manager.
Execution Flow
The generated main.go keeps the entry point small:
- load environment configuration
- handle skip-boot commands when applicable
- register embedded frontend assets when Web UI is enabled
- initialize the App through
wire.InitializeApplication() - call
app.Run(nil, args)
Inside App.Run, the App:
- parses generated CLI commands
- attaches a cancellable context
- begins an inspect record for command execution
- starts the App lifecycle
- runs the selected command
- shuts the App down with a bounded timeout
- finishes the inspect record
flowchart LR construct["construct App"] --> parse["parse command"] parse --> inspect["begin inspect"] inspect --> startup["BeforeStartup -> Startup -> AfterStartup"] startup --> command["run selected command"] command --> shutdown["BeforeShutdown -> Shutdown -> AfterShutdown"] shutdown --> finish["finish inspect"]
Lifecycle Phases
Generated Apps use the lifecycle manager in internal/app.
Startup phases run in registration order:
BeforeStartupStartupAfterStartup
Shutdown phases run in reverse registration order:
BeforeShutdownShutdownAfterShutdown
Startup runs once. Shutdown runs only after startup has completed.
Register Hooks
The primary user extension point is:
internal/app/lifecycle_registry.goUse this file when application code needs startup or shutdown behavior.
package app
type LifecycleRegistry struct {
reportService *reports.Service
}
func NewLifecycleRegistry(reportService *reports.Service) *LifecycleRegistry {
return &LifecycleRegistry{reportService: reportService}
}
func (r *LifecycleRegistry) Register(lifecycle *Lifecycle) {
lifecycle.On(Startup, func(ctx context.Context) error {
return r.reportService.WarmCache(ctx)
})
lifecycle.On(Shutdown, func(ctx context.Context) error {
return r.reportService.Flush(ctx)
})
}NewLifecycleRegistry is built by Wire, so it can receive injected services and repositories.
Framework Hooks
GoForj also registers framework-owned lifecycle hooks during App construction.
Examples include:
- event buses start during startup and close during shutdown
- database connections close during shutdown
- framework-owned queue job handlers are registered during App construction
- cache observers record cache operations into inspects
- event observers record events into inspects and metrics
These hooks are registered explicitly in the generated App wiring. They are not hidden runtime registrations.
App-owned handlers and subscribers should still be registered through documented App extension points before their runtime starts.
Shutdown Timeouts
The App resolves timeout policy once near the root runtime.
Important variables include:
APP_SHUTDOWN_TIMEOUT=30s
SCHEDULER_SUBPROCESS_SHUTDOWN_TIMEOUT=90s
QUEUE_SHUTDOWN_TIMEOUT=10sThe scheduler subprocess path can use a scheduler-specific shutdown timeout. Normal App shutdown uses the App shutdown timeout.
Runtime Boundaries
The lifecycle applies to every generated command, but not every command starts the same long-running work.
Examples:
forj route:liststarts the App lifecycle, lists routes, and shuts down.forj apistarts the HTTP runtime and blocks until interrupted.forj workerstarts worker runtime behavior and blocks until interrupted.forj schedulerstarts scheduler runtime behavior and blocks until interrupted.forj appstarts enabled runtimes together through the runtime host.
Runtime-specific behavior belongs near the runtime package that owns it.
Common Mistakes
Common mistakes
- Do not put startup behavior in
main.gowhen it belongs ininternal/app/lifecycle_registry.go. - Do not make required dependencies appear optional. Construction and lifecycle behavior should expose invalid setup clearly.
- Do not run long-lived runtime startup from random constructors.
- Do not scatter shutdown behavior across package globals.
Next Steps
- Runtime Topology explains combined and split runtime process shapes.
- Project Structure explains where runtime packages live.
