How n8n Uses Dependency Injection with @n8n/di: A Complete Technical Guide
n8n implements dependency injection through a lightweight custom container in the @n8n/di package, using the @Service() decorator and reflect-metadata to automatically wire constructor dependencies without manual instantiation.
The n8n workflow automation platform manages a complex codebase across multiple packages, requiring a robust system for service instantiation and lifecycle management. Understanding how n8n dependency injection works with @n8n/di reveals how the platform maintains modularity, testability, and clean separation of concerns across its execution engine, CLI, and core services.
Core Architecture of @n8n/di
The @n8n/di package provides a minimal yet complete dependency injection container that relies on TypeScript decorators and metadata reflection rather than external frameworks.
The Service Decorator
The @Service() decorator marks classes for dependency injection management. When applied to a class, the decorator registers the class in a global Map called instances within the container. This metadata storage tracks both the class constructor and any optional factory functions.
In packages/@n8n/di/src/di.ts, the decorator implementation stores registration metadata that the container later uses to resolve dependencies:
@Service()
export class ExecutionContextService {
constructor(
private readonly logger: Logger,
private readonly cipher: Cipher,
) {}
}
The Container Singleton
The Container object exported from @n8n/di is a singleton instance of ContainerClass that coordinates all dependency resolution. Rather than using new operators throughout the codebase, n8n services request instances through Container.get(ServiceClass), ensuring consistent singleton behavior and dependency wiring.
The container maintains internal state through two primary mechanisms: the instances Map for registration metadata and a resolutionStack array for tracking the current dependency resolution chain.
How Dependency Resolution Works
The dependency injection container automates object instantiation through constructor parameter inspection and recursive resolution.
Registration and Metadata Storage
When Container.get() is invoked, the container first checks the instances Map for an existing instance. If none exists, it retrieves the constructor and inspects its parameters using reflect-metadata. The Reflect.getMetadata('design:paramtypes', type) call extracts the TypeScript type information for constructor arguments, which serve as service identifiers.
This process occurs in packages/@n8n/di/src/di.ts between lines 89-99, where the container maps constructor parameters to their respective service identifiers.
Constructor Injection with reflect-metadata
The container recursively resolves each constructor dependency before instantiating the target class. For the TracingService example in packages/core/src/observability/tracing/tracing.ts:
import { Service } from '@n8n/di';
import { Logger } from '@n8n/backend-common';
@Service()
export class TracingService {
constructor(private readonly logger: Logger) {}
public startSpan(name: string) {
this.logger.info(`Span start: ${name}`);
}
}
When Container.get(TracingService) executes, the container automatically resolves Logger first, then passes it to the TracingService constructor.
Circular Dependency Detection
The container implements circular dependency detection through the resolutionStack array. During resolution, the container pushes each service identifier onto the stack. If the same identifier appears twice in the stack, the container throws a DIError with a descriptive trace indicating the circular chain.
This protection prevents infinite recursion when services accidentally depend on each other, such as Service A depending on Service B, which depends on Service A.
Practical Implementation Examples
The n8n codebase demonstrates several patterns for defining, consuming, and testing services with @n8n/di.
Defining a Service
Services are defined using the @Service() decorator without manual constructor binding:
// packages/core/src/execution-engine/execution-context.service.ts
import { Service } from '@n8n/di';
import { Logger } from '@n8n/backend-common';
@Service()
export class ExecutionContextService {
constructor(
private readonly logger: Logger,
private readonly cipher: Cipher,
) {}
public createContext() {
this.logger.debug('Creating execution context');
// Implementation...
}
}
Consuming a Service
Rather than instantiating services directly, code retrieves them from the container:
import { Container } from '@n8n/di';
import { ExecutionContextService } from '@/execution-engine/execution-context.service';
export async function handleRequest(req: Request) {
const contextService = Container.get(ExecutionContextService);
contextService.createContext();
// ...
}
This pattern appears throughout packages/cli/src/server.ts, where the CLI initializes by retrieving services like WorkflowService and UserService from the container.
Custom Factories
For complex initialization logic, services can provide custom factory functions:
@Service({
factory: (logger: Logger) => new SomeSpecialService(logger, { mode: 'debug' }),
})
export class SomeSpecialService {
constructor(
private readonly logger: Logger,
private readonly options: { mode: string }
) {}
}
The factory receives resolved dependencies as arguments and returns the configured instance.
Testing with Mock Overrides
The container supports manual service replacement for unit testing:
import { Container } from '@n8n/di';
import { ExecutionContextService } from '@/execution-engine/execution-context.service';
test('execution context merges correctly', () => {
const mockCipher = { encrypt: jest.fn(), decrypt: jest.fn() };
Container.set(Cipher, mockCipher as any);
const service = Container.get(ExecutionContextService);
// Assertions...
});
Container.set() swaps the implementation for the test scope, while Container.reset() clears instantiated objects without losing registrations, ensuring test isolation.
Summary
- @n8n/di provides a lightweight, custom dependency injection container that uses TypeScript decorators and
reflect-metadatafor automatic constructor injection. - The
@Service()decorator registers classes in a global metadata store, whileContainer.get()resolves dependencies recursively by inspecting constructor parameter types. - Circular dependencies are detected at runtime through a resolution stack, throwing descriptive errors before infinite recursion occurs.
- Services are consumed throughout n8n's codebase—from
packages/cli/src/server.tstopackages/core/src/execution-engine/execution-context.service.ts—without direct instantiation, enabling loose coupling and testability. - The container supports custom factories for complex initialization and manual overrides via
Container.set()for testing scenarios.
Frequently Asked Questions
How does n8n handle circular dependencies in its DI container?
The @n8n/di container tracks the current resolution chain in a resolutionStack array. When resolving a service, the container pushes the service identifier onto the stack. If the same identifier appears twice during resolution, the container immediately throws a DIError with a detailed trace showing the circular dependency chain, preventing infinite recursion.
What is the difference between @Service() and manual instantiation in n8n?
Using @Service() registers the class with the DI container in packages/@n8n/di/src/di.ts, storing metadata in the global instances Map. This enables automatic dependency resolution through constructor injection. Manual instantiation with new bypasses the container, requiring developers to manually provide all dependencies and breaking the singleton pattern that ensures consistent service instances across the application.
How can I override a service implementation for unit testing in n8n?
Use Container.set(ServiceClass, mockInstance) to replace the registered implementation with a mock or stub. This method preserves the factory metadata while substituting the instance. After testing, call Container.reset() to clear instantiated objects without losing registrations, ensuring test isolation. This pattern is demonstrated in packages/@n8n/di/src/__tests__/di.test.ts.
Where does n8n initialize its DI container in the application lifecycle?
The container initializes lazily as services are requested, but the CLI entry point in packages/cli/src/server.ts represents the primary initialization site where core services like WorkflowService and UserService are first retrieved from the container. The global Container singleton is imported from @n8n/di and used throughout the codebase to resolve dependencies on demand rather than at application startup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →