Advanced Usage
Learn advanced patterns and techniques for using 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:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
@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:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
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
Repository Pattern (@di-framework/repo)
For larger applications, using the Repository pattern with @di-framework/repo helps maintain a clean separation between business logic and data access.
Standard Repository
import { Repository, InMemoryRepository } from '@di-framework/repo';
@Repository()
export class UserRepository extends InMemoryRepository<User, number> {
async findByEmail(email: string): Promise<User | null> {
const all = await this.findAll();
return all.find((u) => u.email === email) || null;
}
}
Soft Delete Support
import { SoftDeleteRepository, SoftDeletable } from '@di-framework/repo';
interface Product extends SoftDeletable {
id: string;
name: string;
}
@Repository()
export class ProductRepository extends SoftDeleteRepository<Product, string> {
// Implement required abstract methods for your adapter
}
See the Repositories documentation for more details.
Lifecycle Methods
Services can implement lifecycle methods for initialization and context management:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
@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();
}
}
Event-Driven Architecture
The framework supports cross-platform event-driven patterns using the @Publisher and @Subscriber decorators, allowing services to communicate loosely through the container.
Emitting Events with @Publisher
Use @Publisher to emit a custom event when a method is called. By default, it emits after the method executes successfully or fails.
@Container()
export class UserService {
@Publisher({ event: 'user.created', phase: 'after', logging: true })
async createUser(dto: any) {
// ...
return { id: 1, ...dto };
}
}
Receiving Events with @Subscriber
Use @Subscriber to automatically listen for custom events. Subscribed methods receive a payload containing the className, methodName, args, startTime, endTime, result, and error.
@Container()
export class NotificationService {
@Subscriber('user.created')
onUserCreated(event: any) {
if (event.result) {
console.log(`Sending welcome email to ${event.result.name}`);
}
}
}
To publish those same events to Kafka or NATS (or consume remote topics back onto the bus), use @di-framework/events. For WebSocket, TCP, or UDP peers (including a secure channel and Cloudflare Workers / Durable Objects), use @di-framework/socket.
Telemetry and Monitoring
The framework provides built-in support for tracking method execution using the @Telemetry and @TelemetryListener decorators.
Tracking Methods with @Telemetry
Use @Telemetry to track execution of any method in an injectable service. It automatically captures:
@Container()
export class ApiService {
@Telemetry({ logging: true }) // Logs duration and status to console
async fetchData(id: string) {
const response = await fetch(`https://api.example.com/data/${id}`);
return response.json();
}
}
Listening to Events with @TelemetryListener
Use @TelemetryListener to create monitoring services that react to telemetry events from across your application:
@Container()
export class MonitoringService {
@TelemetryListener()
onTelemetry(event: any) {
const { className, methodName, startTime, endTime, error } = event;
const duration = endTime - startTime;
if (error) {
console.error(`Alert: ${className}.${methodName} failed after ${duration}ms:`, error);
} else {
console.log(`Metric: ${className}.${methodName} took ${duration}ms`);
}
// Send to external monitoring service (e.g., Datadog, Sentry)
metricsService.timing(`${className}.${methodName}`, duration);
}
}
Custom Containers
Create isolated containers for different parts of your application:
import { Container as DIContainer } from '@di-framework/core/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:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
// 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:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
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/core/decorators';
import { container } from '@di-framework/core/container';
class EmailService {
constructor(
@Component(LoggerService) private logger: LoggerService,
private sender: string,
) {}
}
const emailer = container.construct(EmailService, {
1: '
[email protected]',
});
Why: Useful for ad-hoc utilities, one-off jobs, or tests where you need DI-managed dependencies plus specific literal parameters.
Configuration Services
For typed, validated configuration from env and files, use @di-framework/config. A minimal imperative example:
import { loadAndRegisterConfig, envSource } from '@di-framework/config';
import { Container, Component } from '@di-framework/core/decorators';
await loadAndRegisterConfig({
defaults: {
database: { host: 'localhost', port: 5432 },
},
sources: [envSource({ prefix: 'APP_' })],
});
@Container()
export class DatabaseService {
constructor(@Component('config') private config: { database: { host: string } }) {
console.log('DB Config:', this.config.database);
}
}
You can still register a plain factory with registerFactory('config', …) when you do not need sources or validation.
Conditional Service Registration
Register different implementations based on environment:
import { useContainer } from '@di-framework/core/container';
const container = useContainer();
// 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: 09 August 2026