Dependency Injection Providers
DI Providers implement the Container interface from @stratix/core, providing dependency injection capabilities for your Stratix application.
Awilix Container
Package: @stratix/di-awilix
The recommended dependency injection container for Stratix, based on the popular Awilix library.
Installation
npm install @stratix/di-awilix
Or using the CLI:
stratix add awilix
Features
- ✅ Singleton, Scoped, and Transient lifetimes
- ✅ Factory functions for flexible service creation
- ✅ Scoped containers for request-level dependencies
- ✅ Auto-wiring for class dependencies
- ✅ Disposal support for cleanup
- ✅ Type-safe with TypeScript
Basic Usage
import { AwilixContainer } from '@stratix/di-awilix';
import { ServiceLifetime } from '@stratix/core';
// Create container
const container = new AwilixContainer();
// Register a singleton
container.register('logger', () => new Logger(), {
lifetime: ServiceLifetime.SINGLETON
});
// Register with dependencies
container.register('userService', (context) => {
const logger = context.resolve<Logger>('logger');
const repository = context.resolve<UserRepository>('userRepository');
return new UserService(logger, repository);
}, {
lifetime: ServiceLifetime.SINGLETON
});
// Resolve services
const userService = container.resolve<UserService>('userService');
Service Lifetimes
Singleton
Created once and reused for all resolutions:
container.register('database', () => new Database(), {
lifetime: ServiceLifetime.SINGLETON
});
// Or using the shorthand
container.singleton('database', new Database());
Scoped
Created once per scope (e.g., per HTTP request):
container.register('requestContext', () => new RequestContext(), {
lifetime: ServiceLifetime.SCOPED
});
// Or using the shorthand
container.scoped('requestContext', () => new RequestContext());
// Create a scope
const scope = container.createScope();
const ctx1 = scope.resolve<RequestContext>('requestContext');
const ctx2 = scope.resolve<RequestContext>('requestContext');
// ctx1 === ctx2 (same instance within scope)
await scope.dispose(); // Cleanup
Transient
Created every time it's resolved:
container.register('requestId', () => crypto.randomUUID(), {
lifetime: ServiceLifetime.TRANSIENT
});
// Or using the shorthand
container.transient('requestId', () => crypto.randomUUID());
Class Registration
Register classes with auto-wiring:
class UserService {
constructor(
private logger: Logger,
private repository: UserRepository
) {}
}
// Register class
container.registerClass(UserService, {
token: 'userService',
lifetime: ServiceLifetime.SINGLETON,
injectionMode: 'PROXY' // or 'CLASSIC'
});
Advanced Features
Conditional Registration
if (!container.has('logger')) {
container.singleton('logger', new ConsoleLogger());
}
Try Resolve
const logger = container.tryResolve<Logger>('logger');
if (logger) {
logger.info('Logger available');
}
Bulk Registration
container.registerAll({
logger: new ConsoleLogger(),
config: new AppConfig(),
cache: new RedisCache()
});
Disposal
class DatabaseConnection {
async dispose() {
await this.close();
}
}
container.singleton('db', new DatabaseConnection());
// Later, cleanup all disposable services
await container.dispose();
Using with Stratix Runtime
The runtime automatically provides a container:
import { StratixApp } from '@stratix/runtime';
import { AwilixContainer } from '@stratix/di-awilix';
const app = new StratixApp({
container: new AwilixContainer(),
plugins: [
// ... plugins
]
});
// Access container
const container = app.getContainer();
Integration with Plugins
Plugins can register services during initialization:
import type { Plugin, PluginContext } from '@stratix/core';
export class DatabasePlugin implements Plugin {
readonly metadata = {
name: 'database',
version: '1.0.0'
};
async initialize(context: PluginContext): Promise<void> {
const config = context.getConfig<DatabaseConfig>();
const database = new Database(config);
// Register in container
context.container.register('database', () => database, {
lifetime: ServiceLifetime.SINGLETON
});
}
}
Container Interface
All DI providers implement the Container interface:
interface Container {
register<T>(
token: Token<T>,
factory: Factory<T>,
options?: RegisterOptions
): void;
resolve<T>(token: Token<T>): T;
has<T>(token: Token<T>): boolean;
createScope(): Container;
dispose(): Promise<void>;
}
Creating a Custom DI Provider
To create your own DI container provider:
import type { Container, Token, Factory, RegisterOptions } from '@stratix/core';
export class CustomContainer implements Container {
private services = new Map<string, any>();
register<T>(
token: Token<T>,
factory: Factory<T>,
options?: RegisterOptions
): void {
const key = this.getKey(token);
this.services.set(key, { factory, options });
}
resolve<T>(token: Token<T>): T {
const key = this.getKey(token);
const service = this.services.get(key);
if (!service) {
throw new Error(`Service not registered: ${key}`);
}
return service.factory(this);
}
has<T>(token: Token<T>): boolean {
return this.services.has(this.getKey(token));
}
createScope(): Container {
// Create scoped container
return new CustomContainer();
}
async dispose(): Promise<void> {
// Cleanup
}
private getKey<T>(token: Token<T>): string {
if (typeof token === 'string') return token;
if (typeof token === 'symbol') return token.toString();
return token.name;
}
}
Best Practices
1. Use Interfaces as Tokens
// Define interface
interface IUserRepository {
findById(id: string): Promise<User | null>;
}
// Register with string token
container.register<IUserRepository>(
'userRepository',
() => new PostgresUserRepository()
);
// Resolve with type
const repo = container.resolve<IUserRepository>('userRepository');
2. Avoid Circular Dependencies
// ❌ Bad: Circular dependency
class ServiceA {
constructor(private serviceB: ServiceB) {}
}
class ServiceB {
constructor(private serviceA: ServiceA) {}
}
// ✅ Good: Use factory or refactor
container.register('serviceA', (ctx) => {
const serviceB = ctx.resolve<ServiceB>('serviceB');
return new ServiceA(serviceB);
});
3. Use Scopes for Request-Level State
// In HTTP plugin
app.use((req, res, next) => {
const scope = container.createScope();
scope.register('request', () => req, {
lifetime: ServiceLifetime.SCOPED
});
req.scope = scope;
res.on('finish', async () => {
await scope.dispose();
});
next();
});
Next Steps
- Dependency Injection Concepts - Learn DI principles
- Creating Plugins - Use DI in plugins
- CQRS - Inject handlers and buses