# How n8n Uses Dependency Injection with @n8n/di: A Complete Technical Guide

> Discover how n8n leverages dependency injection with its @n8n/di container. Learn to use @Service and reflect-metadata for automatic dependency wiring in this technical guide.

- Repository: [n8n - Workflow Automation/n8n](https://github.com/n8n-io/n8n)
- Tags: deep-dive
- Published: 2026-02-24

---

**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:

```typescript
@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`](https://github.com/n8n-io/n8n/blob/main/packages/core/src/observability/tracing/tracing.ts):

```typescript
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:

```typescript
// 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:

```typescript
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`](https://github.com/n8n-io/n8n/blob/main/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:

```typescript
@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:

```typescript
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-metadata` for automatic constructor injection.
- The **`@Service()`** decorator registers classes in a global metadata store, while **`Container.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.ts`](https://github.com/n8n-io/n8n/blob/main/packages/cli/src/server.ts) to [`packages/core/src/execution-engine/execution-context.service.ts`](https://github.com/n8n-io/n8n/blob/main/packages/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`](https://github.com/n8n-io/n8n/blob/main/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.