Advanced Usage
Learn advanced patterns and techniques for using the DI framework effectively.
Transient (Non-Singleton) Services
By default, all services are singletons - the same instance is reused. For services that need a new instance each time, use singleton: false:
@Container({ singleton: false })
export class RequestContext {
id = Math.random().toString();
constructor(@Component(LoggerService) private logger: LoggerService) {
this.logger.log(`Request context created: ${this.id}`);
}
}
// Each resolve creates a new instance
const ctx1 = container.resolve(RequestContext); // new instance
const ctx2 = container.resolve(RequestContext); // different instance
console.log(ctx1.id !== ctx2.id); // true
When to use transient services:
Factory Functions
Register services using factory functions for complex initialization logic:
container.registerFactory('apiClient', () => {
return new HttpClient({
baseUrl: process.env.API_URL,
timeout: 5000,
headers: {
'Authorization': `Bearer ${process.env.API_TOKEN}`
}
});
}, { singleton: true });
// Use in services
@Container()
export class UserService {
constructor(@Component('apiClient') private api: any) {}
async getUser(id: string) {
return this.api.get(`/users/${id}`);
}
}
Factory function benefits:
Initialize services with environment variables
Create instances with complex configuration
Conditionally create different implementations
Integrate third-party libraries
Lifecycle Methods
Services can implement lifecycle methods for initialization and context management:
@Container()
export class DatabaseService {
private connected = false;
private dbUrl: string = '';
setEnv(env: Record<string, any>) {
// Called to initialize environment-specific config
this.dbUrl = env.DATABASE_URL;
console.log('DB URL configured:', this.dbUrl);
}
setCtx(context: any) {
// Called to set execution context
console.log('Context set:', context);
}
connect() {
this.connected = true;
console.log('Connected to:', this.dbUrl);
}
}
// Usage
const db = container.resolve(DatabaseService);
db.setEnv(process.env);
db.setCtx({ userId: '123' });
db.connect();
Multiple Dependencies
Inject multiple dependencies using constructor parameters:
@Container()
export class ApplicationContext {
constructor(
@Component(DatabaseService) private db: DatabaseService,
@Component(LoggerService) private logger: LoggerService,
@Component(AuthService) private auth: AuthService,
@Component(CacheService) private cache: CacheService,
@Component(EmailService) private email: EmailService
) {}
async initialize() {
this.logger.log('Initializing application...');
await this.db.connect();
this.auth.setup();
this.cache.connect();
}
}
Custom Containers
Create isolated containers for different parts of your application:
import { Container as DIContainer } from 'di-framework/container';
// Create custom containers
const apiContainer = new DIContainer();
const workerContainer = new DIContainer();
// Register services in specific containers
@Container({ container: apiContainer })
export class ApiService {
// Only available in apiContainer
}
@Container({ container: workerContainer })
export class WorkerService {
// Only available in workerContainer
}
// Resolve from specific containers
const apiService = apiContainer.resolve(ApiService);
const workerService = workerContainer.resolve(WorkerService);
Use cases for custom containers:
Multi-tenant applications
Plugin systems
Testing with isolated environments
Microservices within a monorepo
Fork Containers (Prototype Pattern)
Clone an existing container and optionally carry over singleton instances:
// Seed the base container
container.register(DatabaseService);
container.register(LoggerService);
const sharedDb = container.resolve(DatabaseService);
// Create an isolated fork for a tenant/request
const tenantContainer = container.fork({ carrySingletons: true });
tenantContainer.registerFactory('config', () => loadTenantConfig());
// Resolves share the DatabaseService instance but have their own registrations
const tenantCtx = tenantContainer.resolve(ApplicationContext);
Why: Quickly spin up scoped containers without re-registering every service. Carry over expensive singletons (DB connections) while keeping registrations isolated.
Observability with Container Events
Use the observer hooks to add diagnostics or metrics around registration and resolution:
const stop = container.on('resolved', ({ key, singleton, fromCache }) => {
const name = typeof key === 'string' ? key : key.name;
metrics.increment('di.resolve', { name, singleton, fromCache });
});
// Later, if needed:
stop();
Use cases:
Log or trace dependency graphs during debugging
Emit metrics for cache hit/miss on singletons
Enforce policies (e.g., warn on transient resolutions in hot paths)
Construct with Overrides (Constructor Pattern)
Create fresh instances without registering them, and override constructor arguments for primitives or config:
import { Component } from 'di-framework/decorators';
import { container } from 'di-framework/container';
class EmailService {
constructor(@Component(LoggerService) private logger: LoggerService, private sender: string) {}
}
const emailer = container.construct(EmailService, { 1: 'no-reply@example.com' });
Why: Useful for ad-hoc utilities, one-off jobs, or tests where you need DI-managed dependencies plus specific literal parameters.
Configuration Services
Create configuration services using factory functions:
// Register configuration
container.registerFactory('config', () => ({
database: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
name: process.env.DB_NAME || 'myapp'
},
api: {
key: process.env.API_KEY,
secret: process.env.API_SECRET
},
features: {
enableNewUI: process.env.ENABLE_NEW_UI === 'true',
maxUploadSize: parseInt(process.env.MAX_UPLOAD_SIZE || '10485760')
}
}), { singleton: true });
// Use in services
@Container()
export class DatabaseService {
constructor(@Component('config') private config: any) {
console.log('DB Config:', this.config.database);
}
}
Conditional Service Registration
Register different implementations based on environment:
// Register different implementations
if (process.env.NODE_ENV === 'production') {
container.registerFactory('logger', () => new ProductionLogger(), { singleton: true });
} else {
container.registerFactory('logger', () => new DevelopmentLogger(), { singleton: true });
}
// Services get the right implementation
@Container()
export class UserService {
constructor(@Component('logger') private logger: any) {
this.logger.log('UserService initialized');
}
}
Service Composition
Compose complex services from simpler ones:
@Container()
export class DataAccessLayer {
constructor(
@Component(DatabaseService) private db: DatabaseService,
@Component(CacheService) private cache: CacheService
) {}
async get(key: string) {
// Try cache first
const cached = await this.cache.get(key);
if (cached) return cached;
// Fall back to database
const data = await this.db.query(`SELECT * FROM data WHERE key = '${key}'`);
await this.cache.set(key, data);
return data;
}
}
@Container()
export class BusinessLogicLayer {
constructor(
@Component(DataAccessLayer) private dal: DataAccessLayer,
@Component(ValidationService) private validator: ValidationService
) {}
async processRequest(request: any) {
this.validator.validate(request);
return this.dal.get(request.key);
}
}
Last modified: 23 November 2025