Skip to content

Lifecycle Hooks

Lifecycle hooks let modules run code at specific points during the application lifecycle. Stratal provides two module hooks: one that fires after module initialization and one that fires during shutdown. Individual providers can also opt into automatic cleanup by implementing a disposal hook.

The OnInitialize interface defines an onInitialize() method that runs after all of a module’s providers have been registered in the DI container. Implement it directly on your module class:

import { Module, OnInitialize, ModuleContext } from 'stratal/module'
@Module({
providers: [CacheService],
})
export class CacheModule implements OnInitialize {
onInitialize({ container, logger }: ModuleContext) {
logger.info('CacheModule initialized')
const cache = container.resolve(CacheService)
cache.warmUp()
}
}

The onInitialize method can also be async:

@Module({
providers: [DatabaseService],
})
export class DatabaseModule implements OnInitialize {
async onInitialize({ container, logger }: ModuleContext) {
const db = container.resolve(DatabaseService)
await db.runMigrations()
logger.info('Database migrations complete')
}
}

Common use cases for onInitialize:

  • Seed initial data or warm caches
  • Run database migrations
  • Register conditional bindings (see Providers)
  • Extend services with decorators
  • Connect to external resources
  • Register additional providers dynamically

The OnShutdown interface defines an onShutdown() method that runs when the application is shutting down:

import { Module, OnShutdown, ModuleContext } from 'stratal/module'
@Module({
providers: [DatabaseService],
})
export class DatabaseModule implements OnShutdown {
onShutdown({ container, logger }: ModuleContext) {
const db = container.resolve(DatabaseService)
db.disconnect()
logger.info('Database connection closed')
}
}

Common use cases for onShutdown:

  • Close database connections
  • Flush pending writes
  • Clean up temporary resources
  • Deregister from external services

onShutdown runs on module classes. Individual providers can release their own resources by implementing a disposal hook on the provider itself. When the application shuts down, the container walks every instance it created and calls its disposal hook, so services that hold timers, sockets, or connection pools clean up without the owning module having to resolve and tear them down by hand.

The container recognizes three forms. If more than one is present, it calls them in this order of precedence:

import { Singleton } from 'stratal/di'
@Singleton()
export class ConnectionPool {
private readonly connections: Connection[] = []
async [Symbol.asyncDispose]() {
await Promise.all(this.connections.map((c) => c.close()))
}
}

When the application tears down, the container disposes instances in reverse creation order, so a service can still safely use dependencies that were constructed before it. A disposal hook may be synchronous or async; the container awaits each one before moving on.

Both lifecycle hooks receive a ModuleContext object with two properties:

PropertyTypeDescription
containerContainerThe DI container, used to resolve and register providers
loggerLoggerServiceA logger instance scoped to the module, for structured log output
onInitialize({ container, logger }: ModuleContext) {
// Resolve any registered provider
const config = container.resolve(ConfigService)
// Register additional providers dynamically
container.registerValue(API_URL, config.get('API_URL'))
// Log with the module-scoped logger
logger.info('Feature module ready')
}

Module initialization follows the import tree. Stratal initializes imported modules before the module that imports them:

@Module({
imports: [DatabaseModule, CacheModule],
controllers: [AppController],
})
export class AppModule implements OnInitialize {
onInitialize({ logger }: ModuleContext) {
// DatabaseModule.onInitialize() has already run
// CacheModule.onInitialize() has already run
logger.info('AppModule ready')
}
}

This means leaf modules (those with no imports) initialize first, and the root module initializes last. This ordering guarantees that by the time a module’s onInitialize runs, all of its dependencies are fully set up.

A module can implement both OnInitialize and OnShutdown:

import { Module, OnInitialize, OnShutdown, ModuleContext } from 'stratal/module'
@Module({
providers: [ConnectionPool],
})
export class InfraModule implements OnInitialize, OnShutdown {
onInitialize({ container, logger }: ModuleContext) {
const pool = container.resolve(ConnectionPool)
pool.connect()
logger.info('Connection pool started')
}
onShutdown({ container, logger }: ModuleContext) {
const pool = container.resolve(ConnectionPool)
pool.disconnect()
logger.info('Connection pool stopped')
}
}
  • Modules to learn how modules are structured and composed.
  • Dependency Injection to understand the container that lifecycle hooks interact with.