Container & Dependency Injection
The Container module is the heart of Orchestr's dependency injection system. It provides a powerful IoC (Inversion of Control) container inspired by Laravel's service container, enabling automatic dependency resolution and elegant service management in TypeScript applications.
Overview
What is the Container?
The Container is a sophisticated dependency injection system that manages object creation and lifecycle in your application. Instead of manually instantiating classes and passing dependencies, you register services with the Container, and it automatically resolves and injects dependencies based on TypeScript's type system.
Automatic Dependency Resolution
Leverages TypeScript's reflection system to automatically discover and inject constructor dependencies
Multiple Binding Types
Supports transient bindings, singletons, and instance bindings
Alias Support
Create alternative names for services
Factory Functions
Use factory functions with container-injected dependencies
Type Safety
Full TypeScript support with generic type parameters
Zero Configuration
Why Use Dependency Injection?
Dependency injection offers several critical benefits:
Testability
Easily swap implementations with mocks or stubs in tests
Maintainability
Changes to dependencies don't cascade through your codebase
Flexibility
Switch implementations without modifying consuming code
Separation of Concerns
Classes focus on their responsibilities, not object creation
Reusability
Services can be shared across different parts of your application
Core Concepts
Service Container
The Container acts as a central registry and factory for your application's services. Think of it as a smart object that knows how to create other objects and manages their lifecycles.
import { Container } from '@orchestr-sh/orchestr';
const container = new Container();Bindings
A binding is a registration that tells the Container how to create an instance of a service. There are three primary types:
1. Transient Bindings
Create a new instance every time the service is requested.
container.bind('Logger', () => new Logger());2. Singleton Bindings
Create one instance and reuse it for all subsequent requests.
container.singleton('Database', () => new Database());3. Instance Bindings
Register an already-created instance.
const config = new Config();
container.instance('Config', config);The @Injectable() Decorator
The @Injectable() decorator is crucial for enabling automatic dependency injection. It triggers TypeScript's emitDecoratorMetadata feature, which embeds type information in the compiled JavaScript.
import { Injectable } from '@orchestr-sh/orchestr';
@Injectable()
class UserService {
constructor(private database: Database, private logger: Logger) {}
}Without the decorator, TypeScript won't emit parameter type metadata, and the Container cannot automatically resolve dependencies.
Getting Started
Prerequisites
Before using the Container's dependency injection features, ensure your project is properly configured:
1. Install reflect-metadata
npm install reflect-metadata2. Import reflect-metadata First
In your application's entry point (e.g., index.ts or main.ts), import reflect-metadata as the very first line:
import 'reflect-metadata'; // MUST BE FIRST LINE
import { Application } from '@orchestr-sh/orchestr';
// ... rest of your importsThis is critical - importing it after other modules can cause reflection metadata to be missing.
3. Configure TypeScript
Update your tsconfig.json to enable decorator support:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2015",
"lib": ["ES2015"]
}
}Important: The target must be ES2015 or higher for proper metadata emission.
Basic Setup
Here's a minimal example to get started:
import 'reflect-metadata';
import { Container, Injectable } from '@orchestr-sh/orchestr';
// Define a service
@Injectable()
class Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// Define a service with dependencies
@Injectable()
class UserService {
constructor(private logger: Logger) {}
getUsers(): string[] {
this.logger.log('Fetching users');
return ['Alice', 'Bob', 'Charlie'];
}
}
// Create container and register services
const container = new Container();
container.singleton(Logger);
container.singleton(UserService);
// Resolve and use
const userService = container.make<UserService>(UserService);
const users = userService.getUsers();
// Console output: [LOG] Fetching usersAPI Reference
Container Class
The main Container class provides the following methods:
bind<T>(abstract, concrete?, shared?): void
Register a binding with the Container.
Parameters:
abstract: Abstract- The service identifier (string, symbol, or class)concrete: Concrete<T> | null- Factory function or constructor (optional, defaults to abstract)shared: boolean- Whether to create singleton (optional, defaults to false)
Examples:
// Basic binding with factory
container.bind('Logger', () => new ConsoleLogger());
// Class binding (auto-resolution)
container.bind(Logger);
// Binding with dependencies
container.bind(EmailService, (c) => {
const config = c.make<Config>('Config');
return new SendGridEmailService(config);
});singleton<T>(abstract, concrete?): void
Register a singleton binding. The Container creates only one instance and reuses it.
Parameters:
abstract: Abstract- The service identifierconcrete: Concrete<T> | null- Factory function or constructor (optional)
Examples:
// Singleton with factory
container.singleton('Database', () => new PostgresDatabase());
// Class singleton (auto-resolution)
container.singleton(DatabaseConnection);
// Singleton with container-injected dependencies
container.singleton(UserRepository, (c) => {
const db = c.make<Database>('Database');
return new UserRepository(db);
});make<T>(abstract, parameters?): T
Resolve and return an instance of the given service.
Parameters:
abstract: Abstract- The service identifierparameters: any[]- Optional parameters to override automatic resolution
Returns: Instance of type T
Examples:
// Basic resolution
const logger = container.make<Logger>(Logger);
// Resolution with string abstract
const db = container.make<Database>('Database');
// Override automatic resolution with explicit parameters
const service = container.make<UserService>(UserService, [customLogger]);Usage Examples
Example 1: Basic Service Binding
import 'reflect-metadata';
import { Container, Injectable } from '@orchestr-sh/orchestr';
@Injectable()
class EmailService {
send(to: string, subject: string): void {
console.log(`Sending email to ${to}: ${subject}`);
}
}
const container = new Container();
container.bind(EmailService);
const emailService = container.make<EmailService>(EmailService);
emailService.send('user@example.com', 'Welcome!');Example 2: Automatic Dependency Resolution
The Container automatically resolves nested dependencies:
@Injectable()
class Logger {
log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
}
@Injectable()
class UserRepository {
constructor(private logger: Logger) {}
find(id: number): object {
this.logger.log(`Finding user ${id}`);
return { id, name: 'John Doe' };
}
}
@Injectable()
class UserService {
constructor(
private repository: UserRepository,
private logger: Logger
) {}
getUser(id: number): object {
this.logger.log(`UserService: Getting user ${id}`);
return this.repository.find(id);
}
}
const container = new Container();
container.singleton(Logger);
container.singleton(UserRepository);
container.singleton(UserService);
const userService = container.make<UserService>(UserService);
const user = userService.getUser(1);
// Console output:
// [2026-02-03T...] UserService: Getting user 1
// [2026-02-03T...] Finding user 1Example 3: Using Aliases for Flexibility
Aliases provide abstraction and allow swapping implementations:
interface PaymentProcessor {
process(amount: number): boolean;
}
@Injectable()
class StripePaymentProcessor implements PaymentProcessor {
process(amount: number): boolean {
console.log(`Processing $${amount} via Stripe`);
return true;
}
}
@Injectable()
class PayPalPaymentProcessor implements PaymentProcessor {
process(amount: number): boolean {
console.log(`Processing $${amount} via PayPal`);
return true;
}
}
const container = new Container();
container.singleton(StripePaymentProcessor);
container.singleton(PayPalPaymentProcessor);
// Create alias pointing to current implementation
container.alias(StripePaymentProcessor, 'PaymentProcessor');
const processor = container.make<PaymentProcessor>('PaymentProcessor');
processor.process(99.99);
// Console output: Processing $99.99 via Stripe
// Switch to PayPal without changing consuming code
container.alias(PayPalPaymentProcessor, 'PaymentProcessor');
const newProcessor = container.make<PaymentProcessor>('PaymentProcessor');
newProcessor.process(49.99);
// Console output: Processing $49.99 via PayPalBest Practices
1. Choose the Right Binding Type
Use Transient Bindings (bind) When:
- Service holds request-specific state
- Creating multiple independent instances is necessary
- Service is lightweight and cheap to instantiate
Use Singleton Bindings (singleton) When:
- Service is stateless
- Service is expensive to create
- Service should be shared across the application
- Service manages shared resources (database connections, caches)
2. Keep Constructors Simple
Constructors should only assign dependencies - avoid business logic:
// ✅ GOOD - Simple assignment
@Injectable()
class UserService {
constructor(
private repository: UserRepository,
private logger: Logger
) {}
async createUser(data: any): Promise<User> {
this.logger.log('Creating user');
return this.repository.create(data);
}
}
// ❌ BAD - Business logic in constructor
@Injectable()
class BadUserService {
constructor(
private repository: UserRepository,
private logger: Logger
) {
this.logger.log('UserService initialized');
this.repository.connect(); // Side effect!
}
}3. Prefer Constructor Injection
Constructor injection makes dependencies explicit and enforces that they're always available:
// ✅ GOOD - Constructor injection
@Injectable()
class OrderService {
constructor(private paymentGateway: PaymentGateway) {}
}Troubleshooting
Error: Cannot resolve dependency at position X
Cause: The Container cannot determine the type of a constructor parameter.
Common Reasons:
- Missing
@Injectable()decorator - TypeScript compiler options not configured correctly
reflect-metadatanot imported or imported too late- Parameter is a primitive type (string, number, etc.)
Solutions:
// ✅ Add @Injectable() decorator
@Injectable()
class MyService {
constructor(private dependency: SomeDependency) {}
}
// ✅ Check tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2015"
}
}
// ✅ Import reflect-metadata first
import 'reflect-metadata'; // FIRST LINE
import { Container } from '@orchestr-sh/orchestr';
// ✅ For primitives, use explicit parameters
const service = container.make(MyService, ['primitive-value']);Integration with ServiceProviders
ServiceProviders are the recommended way to organize service registration in Orchestr applications. They work hand-in-hand with the Container.
ServiceProvider Structure
import { ServiceProvider, Application } from '@orchestr-sh/orchestr';
class AppServiceProvider extends ServiceProvider {
/**
* Register bindings with the container
* This runs before boot() is called on any provider
*/
register(): void {
// Register services here
this.app.singleton(Logger, () => new ConsoleLogger());
this.app.singleton(Database);
}
/**
* Bootstrap services after all providers have registered
* This is optional - only needed for initialization logic
*/
boot(): void {
// Perform initialization here
const db = this.app.make<Database>(Database);
db.connect();
}
/**
* Optional: Declare which services this provider offers
*/
provides(): string[] {
return ['Logger', 'Database'];
}
}Complete Example
Here's a full example with multiple providers:
import 'reflect-metadata';
import { Application, ServiceProvider, Injectable } from '@orchestr-sh/orchestr';
// Services
@Injectable()
class Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
@Injectable()
class Database {
connect(): void {
console.log('Database connected');
}
query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
@Injectable()
class UserRepository {
constructor(
private database: Database,
private logger: Logger
) {}
findAll(): string[] {
this.logger.log('Fetching all users');
this.database.query('SELECT * FROM users');
return ['Alice', 'Bob'];
}
}
// Providers
class LoggingServiceProvider extends ServiceProvider {
register(): void {
this.app.singleton(Logger, () => new Logger());
}
}
class DatabaseServiceProvider extends ServiceProvider {
register(): void {
this.app.singleton(Database, () => new Database());
}
boot(): void {
const db = this.app.make<Database>(Database);
db.connect();
}
}
class RepositoryServiceProvider extends ServiceProvider {
register(): void {
this.app.singleton(UserRepository);
}
}
// Bootstrap application
const app = new Application();
app.register(new LoggingServiceProvider(app));
app.register(new DatabaseServiceProvider(app));
app.register(new RepositoryServiceProvider(app));
// Use services
const userRepo = app.make<UserRepository>(UserRepository);
const users = userRepo.findAll();
console.log('Users:', users);
// Console output:
// Database connected
// [LOG] Fetching all users
// Executing: SELECT * FROM users
// Users: [ 'Alice', 'Bob' ]Conclusion
The Container module is the backbone of Orchestr's dependency injection system. By leveraging TypeScript's type system and reflection capabilities, it provides automatic dependency resolution with minimal configuration.
Key Takeaways:
- Use
@Injectable()on all classes that need dependency injection - Choose the appropriate binding type for each service (transient, singleton, instance)
- Organize service registration in ServiceProviders
- Keep constructors simple - only assign dependencies
- Prefer constructor injection for explicit, testable dependencies
- Use factories for complex initialization logic
- Test with separate container instances and mock implementations
With these patterns and best practices, you can build maintainable, testable, and flexible TypeScript applications using Orchestr's powerful Container system.