Lifecycle Hooks
GOE provides a simple, developer-friendly API for hooking into your application's lifecycle. Under the hood, these hooks use Uber's Fx lifecycle system, but with a much simpler interface.
Overview
Lifecycle hooks allow you to:
- Run initialization code after all dependencies are wired
- Perform cleanup before the application shuts down
- Access all GOE services and custom providers
- Control execution order through Fx's dependency graph
Basic Usage
Using Options
The primary way to register hooks is when creating the application:
app := goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
OnStart: []func(context.Context) error{
initializeCache,
seedDatabase,
startBackgroundWorkers,
},
OnStop: []func(context.Context) error{
stopBackgroundWorkers,
saveApplicationState,
},
})
Using WithHooks Helper
For a more fluent API, use the WithHooks
helper:
app := goe.New(
goe.WithHooks(
goe.WithOnStart(func(ctx context.Context) error {
logger := goe.Log()
logger.Info("Starting application")
return nil
}),
goe.WithOnStop(func(ctx context.Context) error {
logger := goe.Log()
logger.Info("Stopping application")
return nil
}),
),
goe.Options{
WithHTTP: true,
WithDB: true,
},
)
Hook Execution Order
Understanding when hooks execute is crucial:
- Dependency Injection Phase: All providers are instantiated and wired
- Module OnStart: Core modules (Config, Log, DB, HTTP, etc.) start in dependency order
- User OnStart Hooks: Your OnStart hooks execute after all modules
- Application Running: HTTP server starts listening, application serves requests
- Shutdown Signal: Application receives shutdown signal (Ctrl+C, SIGTERM, etc.)
- User OnStop Hooks: Your OnStop hooks execute first (in reverse order)
- Module OnStop: Core modules stop in reverse dependency order
Accessing Services in Hooks
All GOE services are available in hooks via global accessors:
app := goe.New(goe.Options{
WithHTTP: true,
WithDB: true,
WithCache: true,
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
// Access configuration
config := goe.Config()
apiKey := config.GetString("API_KEY")
// Access logger
logger := goe.Log()
logger.Info("Initializing with API key", "key_length", len(apiKey))
// Access database
db := goe.DB()
if err := db.Ping(); err != nil {
return fmt.Errorf("database not ready: %w", err)
}
// Access HTTP server
http := goe.HTTP()
http.App().Get("/health", healthHandler)
// Access cache
cache := goe.Cache()
return cache.Set(ctx, "app:started", time.Now().Unix(), 0)
},
},
})
Using Dependency Injection in Hooks
For more complex scenarios, you can use Fx's dependency injection directly:
type Services struct {
fx.In
DB contract.DB
Cache contract.CacheManager
Logger contract.Logger
Config contract.Config
}
app := goe.New(goe.Options{
WithDB: true,
WithCache: true,
Invokers: []any{
func(lc fx.Lifecycle, services Services) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
services.Logger.Info("Starting with injected services")
return initializeWithServices(services)
},
OnStop: func(ctx context.Context) error {
return cleanupServices(services)
},
})
},
},
})
Common Use Cases
Database Migrations
app := goe.New(goe.Options{
WithDB: true,
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
db := goe.DB()
// Auto-migrate models
err := db.AutoMigrate(
&User{},
&Product{},
&Order{},
)
if err != nil {
return fmt.Errorf("migration failed: %w", err)
}
goe.Log().Info("Database migrations completed")
return nil
},
},
})
Background Workers
var workerCtx context.Context
var workerCancel context.CancelFunc
app := goe.New(goe.Options{
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
workerCtx, workerCancel = context.WithCancel(context.Background())
// Start background worker
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-workerCtx.Done():
return
case <-ticker.C:
processBackgroundTasks()
}
}
}()
goe.Log().Info("Background worker started")
return nil
},
},
OnStop: []func(context.Context) error{
func(ctx context.Context) error {
if workerCancel != nil {
workerCancel()
}
// Wait for worker to finish with timeout
select {
case <-time.After(5 * time.Second):
goe.Log().Warn("Worker shutdown timeout")
case <-workerCtx.Done():
goe.Log().Info("Worker stopped gracefully")
}
return nil
},
},
})
External Service Initialization
var redisClient *redis.Client
app := goe.New(goe.Options{
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
config := goe.Config()
redisClient = redis.NewClient(&redis.Options{
Addr: config.GetString("REDIS_ADDR"),
Password: config.GetString("REDIS_PASSWORD"),
DB: config.GetInt("REDIS_DB"),
})
// Test connection
_, err := redisClient.Ping(ctx).Result()
if err != nil {
return fmt.Errorf("redis connection failed: %w", err)
}
goe.Log().Info("Redis connected")
return nil
},
},
OnStop: []func(context.Context) error{
func(ctx context.Context) error {
if redisClient != nil {
return redisClient.Close()
}
return nil
},
},
})
Cache Warming
app := goe.New(goe.Options{
WithDB: true,
WithCache: true,
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
cache := goe.Cache()
db := goe.DB()
// Load frequently accessed data into cache
var configs []AppConfig
if err := db.Find(&configs).Error; err != nil {
return err
}
for _, cfg := range configs {
key := fmt.Sprintf("config:%s", cfg.Key)
if err := cache.Set(ctx, key, cfg.Value, 1*time.Hour); err != nil {
goe.Log().Warn("Failed to cache config", "key", cfg.Key, "error", err)
}
}
goe.Log().Info("Cache warmed up", "items", len(configs))
return nil
},
},
})
Error Handling
OnStart Errors
If an OnStart hook returns an error:
- The application startup is aborted
- Any successfully started components are stopped
- The error is logged and the application exits
app := goe.New(goe.Options{
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
if !isLicenseValid() {
return fmt.Errorf("invalid license key") // This will abort startup
}
return nil
},
},
})
OnStop Errors
If an OnStop hook returns an error:
- The error is logged
- Shutdown continues with remaining hooks
- The application still exits
app := goe.New(goe.Options{
OnStop: []func(context.Context) error{
func(ctx context.Context) error {
if err := saveState(); err != nil {
// Log error but don't prevent shutdown
goe.Log().Error("Failed to save state", "error", err)
return err // Error is logged but shutdown continues
}
return nil
},
},
})
Best Practices
- Keep hooks focused: Each hook should have a single responsibility
- Handle context cancellation: Respect the context timeout
- Log appropriately: Use the logger to track hook execution
- Fail fast on critical errors: Return errors for critical initialization failures
- Graceful degradation: For non-critical features, log warnings instead of failing
app := goe.New(goe.Options{
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
logger := goe.Log()
// Critical initialization - fail if this doesn't work
if err := initializeCriticalService(); err != nil {
return fmt.Errorf("critical service failed: %w", err)
}
// Non-critical feature - log but don't fail
if err := initializeOptionalFeature(); err != nil {
logger.Warn("Optional feature unavailable", "error", err)
// Continue without failing
}
return nil
},
},
})
Testing Hooks
You can test hooks by creating a test application:
func TestMyHook(t *testing.T) {
var hookExecuted bool
app := goe.New(goe.Options{
WithDB: false, // Minimal setup for testing
OnStart: []func(context.Context) error{
func(ctx context.Context) error {
hookExecuted = true
return nil
},
},
})
ctx := context.Background()
err := app.Start(ctx)
require.NoError(t, err)
assert.True(t, hookExecuted)
err = app.Stop(ctx)
require.NoError(t, err)
}
Advanced: Custom Module with Hooks
For complex initialization, consider creating a custom module:
type BackgroundWorkerModule struct {
workers []Worker
logger contract.Logger
}
func NewBackgroundWorkerModule(logger contract.Logger) contract.Module {
return &BackgroundWorkerModule{
logger: logger,
}
}
func (m *BackgroundWorkerModule) Name() string {
return "background-workers"
}
func (m *BackgroundWorkerModule) OnStart(ctx context.Context) error {
for _, worker := range m.workers {
if err := worker.Start(ctx); err != nil {
return fmt.Errorf("failed to start worker %s: %w", worker.Name(), err)
}
}
m.logger.Info("All workers started", "count", len(m.workers))
return nil
}
func (m *BackgroundWorkerModule) OnStop(ctx context.Context) error {
for _, worker := range m.workers {
if err := worker.Stop(ctx); err != nil {
m.logger.Error("Failed to stop worker", "name", worker.Name(), "error", err)
}
}
return nil
}
Summary
GOE's lifecycle hooks provide a clean, simple API built on top of Fx's powerful lifecycle management. Whether you need to initialize services, start background workers, or perform cleanup, hooks give you the control you need at exactly the right moment in your application's lifecycle.