Options patterns
Every Titan module is configured through one of four static factory methods. They are intentionally repetitive across modules — the shape stays the same so muscle memory transfers from one module to the next. This page gives you the canonical examples plus a support matrix so you know which factory each module actually implements.
Support matrix
@omnitron-dev/titan-*Maintained by the Omnitron team. Independent npm package.
| Module | forRoot | forRootAsync | forFeature | forWorker |
|---|---|---|---|---|
titan-auth | ✓ | ✓ | — | — |
titan-cache | ✓ | ✓ | ✓ | — |
titan-database | ✓ | ✓ | ✓ | — |
titan-discovery | ✓ | — | — | — |
titan-events | ✓ | ✓ | ✓ | — |
titan-health | ✓ | ✓ | ✓ | — |
titan-lock | ✓ | ✓ | — | — |
titan-metrics | ✓ | ✓ | — | — |
titan-notifications | ✓ | ✓ | — | ✓ |
titan-pm | ✓ | ✓ | — | — |
titan-ratelimit | ✓ | ✓ | — | — |
titan-redis | ✓ | ✓ | ✓ | — |
titan-scheduler | ✓ | ✓ | ✓ | — |
titan-telemetry-relay | — | — | — | — |
titan-telemetry-relaydoes not ship a Module class. Usenew TelemetryRelayService(options)directly and wire it via auseValueprovider when DI integration is wanted.
@omnitron-dev/titanconfig + loggerShips inside @omnitron-dev/titan. No additional install required.
Built-in modules follow the same pattern:
| Module | forRoot | forRootAsync | forFeature |
|---|---|---|---|
config | ✓ | ✓ | — |
logger | ✓ | ✓ | — |
forRoot(options) — synchronous configuration
The simplest form. Options are known at boot time.
@Module({
imports: [
TitanRedisModule.forRoot({
config: { host: 'localhost', port: 6379, db: 0 },
}),
TitanCacheModule.forRoot({
defaultTTL: 60,
maxItems: 10_000,
}),
],
})
class AppModule {}
Use when:
- Options come from a literal object or
process.envread. - No other module's services need to compute the options.
forRootAsync({ useFactory, inject?, imports? }) — DI-driven
When the options depend on another module's service — most often
ConfigService — use the async variant.
import { TitanRedisModule } from '@omnitron-dev/titan-redis';
import { CONFIG_SERVICE_TOKEN, ConfigService }
from '@omnitron-dev/titan/module/config';
@Module({
imports: [
TitanRedisModule.forRootAsync({
useFactory: (config: ConfigService) => ({
config: {
host: config.get('redis.host'),
port: config.get('redis.port'),
db: config.get('redis.db'),
},
}),
inject: [CONFIG_SERVICE_TOKEN],
}),
],
})
class AppModule {}
The factory:
- Receives the injected dependencies in the order declared in
inject. - Returns the same
optionsshape asforRoot(). - May be sync or async (
async (config) => {...}is fine).
Importing extra modules into the factory scope
If your factory depends on services that aren't globally exported, import their host module:
TitanCacheModule.forRootAsync({
imports: [SecretsModule], // brings SecretsService into scope
useFactory: (secrets: SecretsService) => ({
redis: { url: secrets.get('REDIS_URL') },
}),
inject: [SecretsService],
}),
useClass / useExisting variants
Some modules also accept class-based provider variants:
NotificationsModule.forRootAsync({
useClass: NotificationsOptionsFactory, // implements `createNotificationsOptions(): Options`
})
// Or reuse an existing factory exported by another module:
NotificationsModule.forRootAsync({
useExisting: SharedOptionsFactory,
})
Refer to per-module pages — useClass and useExisting are
supported by titan-notifications but not uniformly by all
modules.
forFeature(...) — register additional resources
forFeature is for adding per-feature configuration after
the global module has been bound. Shape varies per module.
titan-database.forFeature(repositories)
@Module({
imports: [
TitanDatabaseModule.forFeature([
UsersRepository,
OrdersRepository,
]),
],
})
class UsersModule {}
Each repository becomes injectable by class reference; the
underlying Database connection is the one registered by the
global TitanDatabaseModule.forRoot(...).
titan-cache.forFeature(caches)
@Module({
imports: [
TitanCacheModule.forFeature([
{ name: 'users', defaultTTL: 300 },
{ name: 'tokens', defaultTTL: 60 },
]),
],
})
class CacheLayerModule {}
Multiple named caches share the same backing store but have independent TTLs and namespacing.
titan-events.forFeature(emitters)
Adds event metadata / discovery for additional event-emitting
services. See titan-events.
titan-health.forFeature(indicators)
Registers additional IHealthIndicator classes after forRoot:
@Module({
imports: [
TitanHealthModule.forFeature([
StripeHealthIndicator,
SearchClusterIndicator,
]),
],
})
class FeatureModule {}
titan-redis.forFeature(clients)
Adds named Redis clients:
@Module({
imports: [
TitanRedisModule.forFeature(['cache', 'queue']),
],
})
class MultiClientModule {}
Inject via the token factory:
import { getRedisClientToken } from '@omnitron-dev/titan-redis';
constructor(
@Inject(getRedisClientToken('cache')) private cacheRedis: Redis,
@Inject(getRedisClientToken('queue')) private queueRedis: Redis,
) {}
titan-scheduler.forFeature(providers)
Registers additional providers that own @Cron / @Interval /
@Timeout methods, ensuring the scheduler discovers them.
forWorker(...) — separate runtime
titan-notifications ships this fourth factory specifically for
the worker pod — the one that consumes the messaging
transport and actually delivers messages. The producer pod uses
forRoot; the worker pod uses forWorker.
// Worker pod
@Module({
imports: [
NotificationsModule.forWorker({
targetResolver: NOTIFICATION_TARGET_RESOLVER,
persister: NOTIFICATION_PERSISTER,
realtimeSignaler: NOTIFICATION_REALTIME_SIGNALER,
workerOptions: { concurrency: 8 },
}),
],
providers: [
{ provide: NOTIFICATION_TARGET_RESOLVER, useClass: MyTargetResolver },
{ provide: NOTIFICATION_PERSISTER, useClass: MyPersister },
{ provide: NOTIFICATION_REALTIME_SIGNALER,useClass: MyRealtimeSignaler },
],
})
class NotificationsWorkerModule {}
This separation lets you scale producers (web pods) independently from workers (delivery pods).
Global vs scoped modules
Most modules accept isGlobal: true (or global: true on the
returned DynamicModule) — when set, the module's exports are
visible to every other module without explicit re-import.
TitanRedisModule.forRoot({ config, isGlobal: true });
| When | Recommendation |
|---|---|
| Infrastructure (config, logger, redis, database) | isGlobal: true |
| Feature modules (your business logic) | leave scoped |
| Test environments | scoped — easier to override |
Async lifecycle of forRootAsync
The factory is called once per app boot, after its declared
inject: dependencies are resolved. The resolved options are then
used to instantiate the module's services.
Common gotchas
inject:order must match factory arguments. A misordered array silently passes the wrong service to the wrong parameter. Prefer named destructuring + tuple typing:useFactory: (config: ConfigService, redis: Redis) => ({...}),inject: [CONFIG_SERVICE_TOKEN, REDIS_TOKEN],forFeaturebeforeforRoot— the global module has to be bound first orforFeaturewon't find what to attach to.Application.create({ modules: [GlobalModule, FeatureModule] })resolves order from the dependency graph, but if you're loading them inimports: []manually, listforRootfirst.- Hardcoding
redisOptionsin two modules — when you lettitan-cache,titan-lock,titan-discovery, etc. each spin up their own Redis connection, you triple-count connections. ConfigureTitanRedisModuleonce and let the other modules pick up the shared client. - Calling
forRoottwice — the second call typically wins, which is rarely what you want. If you need different options for different scopes, useforFeature(where supported) or consciously override via test fixtures. forRootAsyncfactory throwing. A throw aborts boot with the original error wrapped in aModuleResolutionError. Don't log-and-swallow; let it crash so the operator sees the configuration mistake.
Migration tip — sync to async
Start with forRoot({...}). The moment you need a value from
ConfigService (or any other service), switch the call to
forRootAsync({useFactory, inject}). The options object shape
stays identical — only the wrapper changes.
// Before
TitanRedisModule.forRoot({ config: { url: 'redis://localhost' } })
// After
TitanRedisModule.forRootAsync({
useFactory: (cfg: ConfigService) => ({ config: { url: cfg.get('redis.url') } }),
inject: [CONFIG_SERVICE_TOKEN],
})
See also
- Module map — visual dependency graph
- Tokens & RPC reference — what to put
in
inject: - Modules system / Dynamic modules —
the underlying
DynamicModulemechanism this all sits on - Authoring a module — how to implement these patterns when building your own