← Back to Home

How to Use Factory Functions for Dependency Injection

Updated January 14, 2026
factory functionsdependency injectiontestingseparation of concerns

Using Factory Functions for Dependency Injection

Factory functions create service instances with their dependencies, enabling testability and flexibility.

Basic Factory Pattern

class AudioService {
  constructor(
    private sampleRate: number,
    private channels: number
  ) {}

  calculateDuration(buffer: Buffer): number {
    // ... implementation
  }
}

// Factory function
export function createAudioService(
  sampleRate: number = 24000,
  channels: number = 1
): AudioService {
  return new AudioService(sampleRate, channels);
}

With Multiple Dependencies

class AssetGenerationService {
  constructor(
    private geminiClient: IGeminiClient,
    private assetManager: IAssetManager,
    private articleManager: IArticleManager,
    private audioService: IAudioService
  ) {}

  async generateAssets(article: FullArticle): Promise<void> {
    // ... implementation
  }
}

// Factory function with all dependencies
export function createAssetGenerationService(
  geminiClient: IGeminiClient,
  assetManager: IAssetManager,
  articleManager: IArticleManager,
  audioService: IAudioService
): AssetGenerationService {
  return new AssetGenerationService(
    geminiClient,
    assetManager,
    articleManager,
    audioService
  );
}

Default Dependencies

import { createGeminiClient } from '../ai/gemini-client';
import { createAssetManager } from '../storage/asset-manager';
import { createArticleManager } from '../storage/article-manager';
import { createAudioService } from './audio-service';

export function createAssetGenerationService(
  geminiClient?: IGeminiClient,
  assetManager?: IAssetManager,
  articleManager?: IArticleManager,
  audioService?: IAudioService
): AssetGenerationService {
  return new AssetGenerationService(
    geminiClient ?? createGeminiClient(),
    assetManager ?? createAssetManager(),
    articleManager ?? createArticleManager(),
    audioService ?? createAudioService()
  );
}

Configuration Object

interface ServiceConfig {
  apiTimeout: number;
  maxRetries: number;
  enableCache: boolean;
}

class ApiClient {
  constructor(private config: ServiceConfig) {}
}

export function createApiClient(config: Partial<ServiceConfig> = {}): ApiClient {
  const defaultConfig: ServiceConfig = {
    apiTimeout: 30000,
    maxRetries: 3,
    enableCache: true,
  };

  return new ApiClient({ ...defaultConfig, ...config });
}

For Testing

// Production
const service = createAssetGenerationService();

// Testing
const mockGemini = new MockGeminiClient();
const mockAssets = new MockAssetManager();
const service = createAssetGenerationService(
  mockGemini,
  mockAssets,
  mockArticleManager,
  mockAudioService
);

// Verify calls
await service.generateAssets(article);
expect(mockGemini.generateImageCalls.length).toBe(7);

Singleton Pattern

let instance: YouTubeClient | null = null;

export function createYouTubeClient(authManager: AuthManager): YouTubeClient {
  if (!instance) {
    instance = new YouTubeClient(authManager);
  }
  return instance;
}

// Or with closure
export const createYouTubeClient = (() => {
  let instance: YouTubeClient | null = null;

  return (authManager: AuthManager) => {
    if (!instance) {
      instance = new YouTubeClient(authManager);
    }
    return instance;
  };
})();

Async Factory

class DatabaseService {
  constructor(private connection: Connection) {}

  static async create(connectionString: string): Promise<DatabaseService> {
    const connection = await connect(connectionString);
    return new DatabaseService(connection);
  }
}

// Factory function
export async function createDatabaseService(
  connectionString: string
): Promise<DatabaseService> {
  return await DatabaseService.create(connectionString);
}

Factory with Builder Pattern

class Service {
  private constructor(
    private dependency1: Dep1,
    private dependency2: Dep2,
    private config: Config
  ) {}

  static builder() {
    return new ServiceBuilder();
  }
}

class ServiceBuilder {
  private dep1?: Dep1;
  private dep2?: Dep2;
  private config: Config = defaultConfig();

  withDependency1(dep: Dep1): this {
    this.dep1 = dep;
    return this;
  }

  withDependency2(dep: Dep2): this {
    this.dep2 = dep;
    return this;
  }

  withConfig(config: Partial<Config>): this {
    this.config = { ...this.config, ...config };
    return this;
  }

  build(): Service {
    if (!this.dep1 || !this.dep2) {
      throw new Error('Missing dependencies');
    }
    return new Service(this.dep1, this.dep2, this.config);
  }
}

// Usage
const service = Service.builder()
  .withDependency1(dep1)
  .withDependency2(dep2)
  .withConfig({ timeout: 5000 })
  .build();

Benefits

Aspect Direct Construction Factory Function
Testability Difficult to mock Easy dependency injection
Configuration Hardcoded defaults Flexible parameters
Singletons Manual management Built-in support
Dependency hiding Hidden dependencies Explicit dependencies
Validation In constructor In factory

Best Practices

  1. Always export factory functions from service modules
  2. Use interfaces for dependencies (enforces contracts)
  3. Provide sensible defaults for optional dependencies
  4. Document dependencies clearly in factory signature
  5. Keep factories simple - complex construction in separate builders
  6. Use factories for testability - inject mocks easily
  7. Consistent naming - create{ServiceName}()