Skip to main content

Introduction

Visual Studio Code uses a comprehensive dependency injection (DI) system to manage services throughout the application. This system enables loose coupling, testability, and dynamic service replacement.
Key files:
  • src/vs/platform/instantiation/common/instantiation.ts - DI interfaces
  • src/vs/platform/instantiation/common/instantiationService.ts - DI implementation
  • src/vs/platform/instantiation/common/descriptors.ts - Service descriptors

Core Concepts

Service Identifiers

Services are identified using unique service identifiers created with the createDecorator function:
// src/vs/platform/instantiation/common/instantiation.ts:109
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

// Define the service identifier
export const IMyService = createDecorator<IMyService>('myService');

// Define the service interface
export interface IMyService {
  readonly _serviceBrand: undefined; // Brand for type safety
  doSomething(): void;
  getValue(): string;
}
Service identifiers serve multiple purposes:
  1. Type Safety: TypeScript ensures you inject the correct service type
  2. Runtime Resolution: The DI container looks up services by identifier
  3. Decoration: The identifier acts as a parameter decorator for injection
  4. Uniqueness: String IDs prevent naming collisions
// The identifier is both a type and a decorator
function (target: Function, key: string, index: number) {
  // Stores dependency metadata
}
Every service interface must include readonly _serviceBrand: undefined:
export interface IMyService {
  readonly _serviceBrand: undefined;
  // ... methods
}
This is a TypeScript branding technique that:
  • Prevents different services from being structurally compatible
  • Ensures type safety even if two services have identical methods
  • Is never implemented (it’s just for the type system)

Service Implementation

Basic Service

Here’s how to implement a service:
1

Define the Service Interface

// myService.ts
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';

export const IMyService = createDecorator<IMyService>('myService');

export interface IMyService {
  readonly _serviceBrand: undefined;
  processData(data: string): Promise<string>;
}
2

Implement the Service

import { ILogService } from 'vs/platform/log/common/log';
import { IFileService } from 'vs/platform/files/common/files';

export class MyService implements IMyService {
  declare readonly _serviceBrand: undefined;
  
  constructor(
    @ILogService private readonly logService: ILogService,
    @IFileService private readonly fileService: IFileService
  ) {
    this.logService.info('MyService initialized');
  }
  
  async processData(data: string): Promise<string> {
    this.logService.info('Processing data');
    // Use injected services
    return data.toUpperCase();
  }
}
Important: Service dependencies are declared using the @IServiceName decorator syntax. The DI container will automatically inject these dependencies.
3

Register the Service

// In a registration file (e.g., workbench.common.main.ts)
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IMyService, MyService } from './myService';

registerSingleton(IMyService, MyService, InstantiationType.Delayed);

Service with Non-Service Parameters

Critical Rule: Non-service parameters MUST come BEFORE service parameters in the constructor.
export class ConfigurableService implements IConfigurableService {
  declare readonly _serviceBrand: undefined;
  
  constructor(
    // Non-service parameters come first
    private readonly configValue: string,
    private readonly options: IOptions,
    // Service parameters come after
    @ILogService private readonly logService: ILogService,
    @IFileService private readonly fileService: IFileService
  ) {
    this.logService.info(`ConfigurableService with ${configValue}`);
  }
}

// When creating instances:
const instance = instantiationService.createInstance(
  ConfigurableService,
  'myConfig',      // Non-service arg
  { enabled: true } // Non-service arg
  // Services are injected automatically
);

Instantiation Service

The IInstantiationService is the heart of the DI system:
// src/vs/platform/instantiation/common/instantiation.ts:52
export interface IInstantiationService {
  readonly _serviceBrand: undefined;
  
  // Create an instance with dependencies injected
  createInstance<T>(ctor: new (...args: any[]) => T, ...args: any[]): T;
  
  // Invoke a function with service access
  invokeFunction<R>(fn: (accessor: ServicesAccessor, ...args: any[]) => R, ...args: any[]): R;
  
  // Create a child container with additional services
  createChild(services: ServiceCollection, store?: DisposableStore): IInstantiationService;
  
  // Dispose the instantiation service and all created services
  dispose(): void;
}

Creating Instances

// Create an instance with automatic dependency injection
const myService = instantiationService.createInstance(MyService);

// With additional constructor arguments
const configured = instantiationService.createInstance(
  ConfigurableService,
  'config-value',
  { option: true }
);
The instantiation service:
  1. Analyzes constructor parameters
  2. Resolves service dependencies from the container
  3. Injects services after non-service parameters
  4. Constructs and returns the instance

Service Registration

Services are registered using the registerSingleton function:
import { 
  registerSingleton, 
  InstantiationType 
} from 'vs/platform/instantiation/common/extensions';

// Eager instantiation - created immediately at startup
registerSingleton(
  IMyService, 
  MyService, 
  InstantiationType.Eager
);

// Delayed instantiation - created when first requested
registerSingleton(
  IMyService, 
  MyService, 
  InstantiationType.Delayed
);
Service is created during application startup:
registerSingleton(ILifecycleService, LifecycleService, InstantiationType.Eager);
Use for services that:
  • Must be initialized early
  • Have critical startup logic
  • Are always needed
Service is created lazily when first requested:
registerSingleton(ISearchService, SearchService, InstantiationType.Delayed);
This is the preferred option because:
  • Faster startup time
  • Lower initial memory usage
  • Services only created if actually needed
For delayed services, VS Code uses a proxy pattern:
// From src/vs/platform/instantiation/common/instantiationService.ts:333
return new Proxy(Object.create(null), {
  get(target: any, key: PropertyKey): unknown {
    // Lazy initialization on first property access
    if (!idle.isInitialized) {
      // Handle events specially
      if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {
        // Queue event listeners until service is created
      }
    }
    
    // Create actual instance
    const obj = idle.value;
    let prop = obj[key];
    if (typeof prop !== 'function') {
      return prop;
    }
    prop = prop.bind(obj);
    target[key] = prop;
    return prop;
  }
});

Service Descriptors

For more complex registration scenarios, use SyncDescriptor:
// src/vs/platform/instantiation/common/descriptors.ts:6
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';

// Register with static arguments
const descriptor = new SyncDescriptor(
  MyService,
  ['static-arg-1', 'static-arg-2'],
  true // supportsDelayedInstantiation
);

serviceCollection.set(IMyService, descriptor);

Dependency Resolution

The instantiation service resolves dependencies using a graph-based algorithm:
1

Analyze Dependencies

// src/vs/platform/instantiation/common/instantiationService.ts:236
for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {
  const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
  
  if (!instanceOrDesc) {
    this._throwIfStrict(
      `${id} depends on ${dependency.id} which is NOT registered.`,
      true
    );
  }
  
  if (instanceOrDesc instanceof SyncDescriptor) {
    // Add to dependency graph
    graph.insertEdge(item, { id: dependency.id, desc: instanceOrDesc });
    stack.push(d);
  }
}
2

Detect Cycles

// src/vs/platform/instantiation/common/instantiationService.ts:258
const roots = graph.roots();

if (roots.length === 0) {
  if (!graph.isEmpty()) {
    throw new CyclicDependencyError(graph);
  }
  break;
}
Circular dependencies between services will throw a CyclicDependencyError. The error message includes the dependency chain.
3

Instantiate in Order

// Create services in dependency order
for (const { data } of roots) {
  const instance = this._createServiceInstanceWithOwner(
    data.id,
    data.desc.ctor,
    data.desc.staticArguments,
    data.desc.supportsDelayedInstantiation,
    data._trace
  );
  this._setCreatedServiceInstance(data.id, instance);
  graph.removeNode(data);
}

Service Lifecycle

Service Creation

Service Disposal

// src/vs/platform/instantiation/common/instantiationService.ts:50
dispose(): void {
  if (!this._isDisposed) {
    this._isDisposed = true;
    // Dispose all child containers
    dispose(this._children);
    this._children.clear();
    
    // Dispose all services created by this container
    for (const candidate of this._servicesToMaybeDispose) {
      if (isDisposable(candidate)) {
        candidate.dispose();
      }
    }
    this._servicesToMaybeDispose.clear();
  }
}
Services that implement IDisposable are automatically disposed when the instantiation service is disposed.

Testing with DI

The DI system makes testing easy:
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';

suite('MyService', () => {
  let instantiationService: TestInstantiationService;
  let myService: IMyService;
  
  setup(() => {
    instantiationService = new TestInstantiationService();
    
    // Register mock services
    instantiationService.stub(ILogService, new MockLogService());
    instantiationService.stub(IFileService, new MockFileService());
    
    // Create service under test
    myService = instantiationService.createInstance(MyService);
  });
  
  test('should process data', async () => {
    const result = await myService.processData('test');
    assert.strictEqual(result, 'TEST');
  });
});

Best Practices

// Good: Depend on interface
constructor(
  @ILogService private readonly logService: ILogService
) {}

// Bad: Depend on concrete class
constructor(
  private readonly logService: LogService // Don't do this
) {}
// Preferred: Lazy loading
registerSingleton(IMyService, MyService, InstantiationType.Delayed);

// Only if truly necessary
registerSingleton(ICriticalService, CriticalService, InstantiationType.Eager);
Follow the Single Responsibility Principle:
  • Each service should have one clear purpose
  • Large services should be split into smaller ones
  • Use service composition for complex behavior
export class MyService extends Disposable implements IMyService {
  declare readonly _serviceBrand: undefined;
  
  constructor(
    @IFileService private readonly fileService: IFileService
  ) {
    super();
    
    // Register disposables
    this._register(this.fileService.onDidFilesChange(() => {
      // Handle changes
    }));
  }
}

Common Patterns

Optional Service Dependencies

import { optional } from 'vs/platform/instantiation/common/instantiation';

export class MyService implements IMyService {
  constructor(
    @ILogService private readonly logService: ILogService,
    @optional(ITelemetryService) private readonly telemetryService: ITelemetryService | undefined
  ) {
    if (this.telemetryService) {
      this.telemetryService.publicLog('service.created');
    }
  }
}

Service Accessor Pattern

// Use accessor when you need multiple services temporarily
export function myFunction(accessor: ServicesAccessor, arg: string): void {
  const logService = accessor.get(ILogService);
  const configService = accessor.get(IConfigurationService);
  
  const value = configService.getValue<string>('my.setting');
  logService.info(`Processing ${arg} with ${value}`);
}

// Call via instantiation service
instantiationService.invokeFunction(myFunction, 'test-arg');

Next Steps

Contribution Model

Learn how features register with the DI system

Extension System

Understand how extensions use services