Dependency Injection
Dependency Injection (DI) enables loosely coupled, testable code by automatically providing dependencies to classes.
Deep Dive: See DI Reference for technical details on Container, Binding, and
@inject.
Standalone Package: The core DI container is available as the standalone
@venizia/ignis-inversionpackage for use outside the Ignis framework. See Inversion Package Reference for details.
Core Concepts
| Concept | Description |
|---|---|
| Container | The central registry for all your application's services and dependencies. The Application class itself extends Container. |
| Binding | The process of registering a class or value with the container under a specific key (e.g., 'services.UserService'). |
| Injection | The process of requesting a dependency from the container using the @inject decorator. |
How It Works: The DI Flow
Instantiation Algorithm (Two-Phase)
When the container instantiates a class, it follows a two-phase process:
- Constructor injection -- Reads
@injectmetadata from the class, sorts by parameter index, resolves each dependency from the container, and passes them as constructor arguments. - Property injection -- After construction, reads property metadata and assigns each dependency to the decorated properties.
Binding Dependencies
Before a dependency can be injected, it must be bound to the container. This is typically done in the preConfigure method of your Application class.
Standard Resource Binding
The Application class provides helper methods for common resource types. These automatically create a binding with a conventional key.
| Method | Default Key | Default Scope |
|---|---|---|
app.service(UserService) | services.UserService | Transient |
app.repository(UserRepository) | repositories.UserRepository | Transient |
app.dataSource(PostgresDataSource) | datasources.PostgresDataSource | Singleton |
app.controller(UserController) | controllers.UserController | Transient |
app.component(MyComponent) | components.MyComponent | Singleton |
All these methods accept an optional second parameter to customize the binding key:
// Default binding (key: 'controllers.UserController')
app.controller(UserController);
// Custom binding key
app.controller(UserController, {
binding: { namespace: 'controllers', key: 'CustomUserController' }
});Custom Bindings
For other values or more complex setups, use the bind method directly.
// In your application class's preConfigure()
this.bind<MyCustomClass>({ key: 'MyCustomClass' }).toClass(MyCustomClass);
this.bind<string>({ key: 'API_KEY' }).toValue('my-secret-api-key');Binding Scopes
You can control the lifecycle of your dependencies with scopes.
TRANSIENT(default): A new instance is created every time the dependency is resolved.SINGLETON: A single instance is created once and cached. All subsequent resolutions return the same instance.
import { BindingScopes } from '@venizia/ignis-inversion';
this.bind({ key: 'services.MySingletonService' })
.toClass(MySingletonService)
.setScope(BindingScopes.SINGLETON); // Use SINGLETON for this serviceBinding Tags
Bindings are automatically tagged with their namespace prefix. For example, a binding with key 'services.UserService' is auto-tagged with 'services'. You can also add custom tags:
this.bind({ key: 'services.UserService' })
.toClass(UserService)
.setTags('critical', 'user-domain');Tags are used by the container's findByTag() method to discover bindings by category.
Injecting Dependencies
Ignis provides the @inject decorator to request dependencies from the container.
Constructor Injection (Recommended)
This makes dependencies explicit and ensures they are available right away.
import { BaseRestController, controller, inject } from '@venizia/ignis';
import { UserService } from '../services/user.service';
@controller({ path: '/users' })
export class UserController extends BaseRestController {
constructor(
@inject({ key: 'services.UserService' })
private userService: UserService
) {
super({ scope: UserController.name });
}
// ... you can now use this.userService
}Property Injection
You can also inject dependencies directly as class properties.
import { inject } from '@venizia/ignis';
import { UserService } from '../services/user.service';
export class UserComponent {
@inject({ key: 'services.UserService' })
private userService: UserService;
// ...
}Optional Dependencies
Mark a dependency as optional to avoid errors when it's not bound:
constructor(
@inject({ key: 'services.CacheService', isOptional: true })
private cache?: CacheService
) {
super({ scope: MyService.name });
// cache will be undefined if not bound
}Providers
Providers are used for dependencies that require complex setup logic. A provider can be either a factory function or a class that implements the IProvider<T> interface with a value() method.
Factory Function Provider
// In your application class
this.bind<ThirdPartyApiClient>({ key: 'services.ApiClient' })
.toProvider((container) => {
const config = container.get<IConfig>({ key: 'configs.api' });
return new ThirdPartyApiClient({
apiKey: config.apiKey,
baseUrl: config.baseUrl,
});
});Class-Based Provider
import { IProvider, Container } from '@venizia/ignis-inversion';
export class ApiClientProvider implements IProvider<ThirdPartyApiClient> {
value(container: Container): ThirdPartyApiClient {
const config = container.get<IConfig>({ key: 'configs.api' });
const client = new ThirdPartyApiClient({
apiKey: config.apiKey,
baseUrl: config.baseUrl,
});
client.connect();
return client;
}
}You would then bind this provider in your application:
// In your application class
this.bind<ThirdPartyApiClient>({ key: 'services.ApiClient' })
.toProvider(ApiClientProvider);Standalone Containers
You can create independent DI containers using the Container class directly. These containers are completely separate from the application's context and do not share any bindings.
Creating an Independent Container
import { Container, BindingScopes } from '@venizia/ignis-inversion';
// Create a standalone container
const container = new Container({ scope: 'MyCustomContainer' });
// Bind dependencies
container.bind({ key: 'config.apiKey' }).toValue('my-secret-key');
container.bind({ key: 'services.Logger' }).toClass(LoggerService);
container.bind({ key: 'services.Cache' })
.toClass(CacheService)
.setScope(BindingScopes.SINGLETON);
// Resolve dependencies
const logger = container.get<LoggerService>({ key: 'services.Logger' });
const apiKey = container.get<string>({ key: 'config.apiKey' });
// Resolve multiple at once
const [svcA, svcB] = container.gets<[ServiceA, ServiceB]>({
bindings: [
{ key: 'services.A' },
{ key: 'services.B', isOptional: true },
],
});Use Cases
| Use Case | Description |
|---|---|
| Unit Testing | Create isolated containers with mock dependencies for each test |
| Isolated Modules | Build self-contained modules with their own dependency graph |
| Multi-Tenancy | Separate containers per tenant with tenant-specific configurations |
| Worker Threads | Independent containers for background workers |
| Plugin Systems | Each plugin gets its own container to prevent conflicts |
Example: Testing with Isolated Container
import { Container } from '@venizia/ignis-inversion';
import { describe, it, expect, beforeEach } from 'bun:test';
describe('UserService', () => {
let container: Container;
beforeEach(() => {
// Fresh container for each test
container = new Container({ scope: 'TestContainer' });
// Bind mock dependencies
container.bind({ key: 'repositories.UserRepository' }).toValue({
findById: async () => ({ id: '1', name: 'Test User' }),
});
container.bind({ key: 'services.UserService' }).toClass(UserService);
});
it('should find user by id', async () => {
const userService = container.get<UserService>({ key: 'services.UserService' });
const user = await userService.findById({ id: '1' });
expect(user.name).toBe('Test User');
});
});Container vs Application
| Aspect | Application (extends Container) | Standalone Container |
|---|---|---|
| Purpose | Full HTTP server with routing, middleware | Pure dependency injection |
| Bindings | Shared across entire application | Isolated, no sharing |
| Lifecycle | Managed by framework | You control it |
| Use Case | Main application | Testing, isolated modules, workers |
TIP
The Application class extends Container, so all container methods (bind, get, gets, resolve, findByTag, isBound, unbind) are available on your application instance. Standalone containers are useful when you need isolation from the main application context.
Container API Summary
| Method | Description |
|---|---|
bind<T>({ key }) | Create a new binding |
get<T>({ key, isOptional? }) | Resolve a single dependency |
gets<T>({ bindings }) | Resolve multiple dependencies at once |
resolve<T>(cls) | Instantiate a class with DI (alias for instantiate) |
isBound({ key }) | Check if a key is bound |
unbind({ key }) | Remove a binding |
findByTag({ tag, exclude? }) | Find bindings by tag |
clear() | Clear all cached singleton instances |
reset() | Remove all bindings entirely |
See Also
Related Concepts:
- Application - Application extends Container
- Controllers - Use DI for injecting services
- Services - Use DI for injecting repositories
- Providers - Factory pattern for dynamic injection
References:
- Dependency Injection API - Complete API reference
- Inversion Helper - DI container utilities
- Glossary - DI concepts explained
Tutorials:
- Testing - Unit testing with mocked dependencies
- Building a CRUD API - DI in practice
Best Practices:
- Architectural Patterns - DI patterns and anti-patterns