Loading learning content...
With three dependency injection mechanisms available—Constructor Injection, Setter Injection, and Interface Injection—choosing the right approach for a given scenario becomes a critical architectural decision.
Each mechanism has distinct characteristics that make it suitable for different situations. Making the wrong choice leads to unnecessary complexity, testing difficulties, or architectural constraints. Making the right choice creates elegant, maintainable designs.
This page provides a comprehensive comparison, examining each injection type across multiple dimensions: dependency management, lifecycle, testing, framework integration, and real-world applicability. By the end, you'll have a clear decision framework for selecting the appropriate injection mechanism.
By the end of this page, you will understand the detailed tradeoffs between injection types, when each excels, and have a practical decision tree for choosing the right approach. You'll also see hybrid patterns that combine multiple injection types effectively.
Before diving into detailed comparisons, let's establish the fundamental nature of each injection type:
Constructor Injection:
Setter Injection:
Interface Injection:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
// Shared dependency interfacesinterface UserRepository { findById(id: string): Promise<User | null>; save(user: User): Promise<void>;} interface EventPublisher { publish(event: DomainEvent): Promise<void>;} interface Logger { info(message: string): void; error(message: string, error?: Error): void;} // ========================================// CONSTRUCTOR INJECTION// Dependencies explicit in constructor// ======================================== class ConstructorInjectedUserService { constructor( private readonly userRepository: UserRepository, private readonly eventPublisher: EventPublisher, private readonly logger: Logger ) { // Object is fully initialized after construction // All dependencies are final/readonly } async updateUser(id: string, data: Partial<User>): Promise<User> { // All dependencies guaranteed to exist this.logger.info(`Updating user ${id}`); const user = await this.userRepository.findById(id); if (!user) throw new UserNotFoundError(id); const updatedUser = { ...user, ...data }; await this.userRepository.save(updatedUser); await this.eventPublisher.publish(new UserUpdatedEvent(user, updatedUser)); return updatedUser; }} // Usage: Dependencies passed at constructionconst service1 = new ConstructorInjectedUserService( new PostgresUserRepository(), new RabbitMQEventPublisher(), new ConsoleLogger()); // ========================================// SETTER INJECTION// Dependencies set via setters after construction// ======================================== class SetterInjectedUserService { private userRepository?: UserRepository; private eventPublisher?: EventPublisher; private logger?: Logger; // Setter methods for each dependency setUserRepository(repo: UserRepository): void { this.userRepository = repo; } setEventPublisher(publisher: EventPublisher): void { this.eventPublisher = publisher; } setLogger(logger: Logger): void { this.logger = logger; } async updateUser(id: string, data: Partial<User>): Promise<User> { // Must check for dependencies or use assertions if (!this.userRepository || !this.eventPublisher || !this.logger) { throw new Error('Dependencies not configured'); } this.logger.info(`Updating user ${id}`); const user = await this.userRepository.findById(id); if (!user) throw new UserNotFoundError(id); const updatedUser = { ...user, ...data }; await this.userRepository.save(updatedUser); await this.eventPublisher.publish(new UserUpdatedEvent(user, updatedUser)); return updatedUser; }} // Usage: Object created, then dependencies setconst service2 = new SetterInjectedUserService();service2.setUserRepository(new PostgresUserRepository());service2.setEventPublisher(new RabbitMQEventPublisher());service2.setLogger(new ConsoleLogger()); // ========================================// INTERFACE INJECTION// Dependencies define interfaces, class implements them// ======================================== // Injector interfaces (defined by or alongside dependencies)interface UserRepositoryInjector { injectUserRepository(repo: UserRepository): void;} interface EventPublisherInjector { injectEventPublisher(publisher: EventPublisher): void;} interface LoggerInjector { injectLogger(logger: Logger): void;} class InterfaceInjectedUserService implements UserRepositoryInjector, EventPublisherInjector, LoggerInjector { private userRepository!: UserRepository; private eventPublisher!: EventPublisher; private logger!: Logger; // Implement injector interface methods injectUserRepository(repo: UserRepository): void { this.userRepository = repo; } injectEventPublisher(publisher: EventPublisher): void { this.eventPublisher = publisher; } injectLogger(logger: Logger): void { this.logger = logger; } async updateUser(id: string, data: Partial<User>): Promise<User> { this.logger.info(`Updating user ${id}`); const user = await this.userRepository.findById(id); if (!user) throw new UserNotFoundError(id); const updatedUser = { ...user, ...data }; await this.userRepository.save(updatedUser); await this.eventPublisher.publish(new UserUpdatedEvent(user, updatedUser)); return updatedUser; }} // Usage: Container detects interfaces and injectsconst service3 = new InterfaceInjectedUserService();container.injectDependencies(service3); // Container handles injectionObservations from the code:
Let's examine each injection type across key architectural dimensions:
| Dimension | Constructor Injection | Setter Injection | Interface Injection |
|---|---|---|---|
| Dependency Visibility | Explicit in constructor signature | In setter methods, may be scattered | In implemented interfaces |
| Completeness Guarantee | ✅ Always complete after construction | ❌ May be incomplete | ⚠️ Depends on container |
| Immutability | ✅ Easy (readonly fields) | ❌ Difficult (setters imply mutability) | ⚠️ Possible with one-shot injection |
| Optional Dependencies | ⚠️ Requires overloaded constructors | ✅ Natural fit | ✅ Natural fit |
| Circular Dependencies | ❌ Cannot resolve directly | ✅ Can resolve with cycles | ✅ Can resolve with cycles |
| Testing Simplicity | ✅ Just pass mocks to constructor | ✅ Set mock dependencies | ⚠️ Requires container setup or manual injection |
| Framework Integration | ⚠️ Container must know constructor | ✅ Works well with containers | ✅ Discovery-based, very flexible |
| Boilerplate | ✅ Minimal | ⚠️ Setter per dependency | ❌ Interface + Method per dependency |
| IDE Support | ✅ Excellent (constructor inspection) | ✅ Good (setter navigation) | ⚠️ Moderate (interface implementation) |
| Refactoring Safety | ✅ Compiler catches missing deps | ⚠️ Runtime errors possible | ⚠️ Runtime errors possible |
| Self-Documentation | ✅ Constructor is the spec | ⚠️ Must inspect setters | ⚠️ Must inspect interfaces |
Key insights from the comparison:
Constructor Injection wins for type safety, immutability, and self-documentation. It's the default choice for application code.
Setter Injection wins for optional dependencies and legacy integration where constructors can't be modified.
Interface Injection wins for framework extensibility and discovery-based architectures where the container doesn't know specific types.
Let's explore each dimension in more depth:
Two critical properties for robust dependency management are completeness (the object has all dependencies it needs) and immutability (dependencies cannot be changed after initialization).
Why these matter:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
// ========================================// CONSTRUCTOR INJECTION: Excellent completeness & immutability// ======================================== class CompleteAndImmutableService { // readonly guarantees immutability constructor( private readonly userRepo: UserRepository, private readonly cache: CacheService ) { // Object is complete - no way to create partially initialized // TypeScript enforces all parameters are provided } async getUser(id: string): Promise<User | null> { // No null checks needed - construction guarantees completeness const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; const user = await this.userRepo.findById(id); if (user) await this.cache.set(`user:${id}`, user); return user; }} // This won't compile - forces completeness// const broken = new CompleteAndImmutableService(); // Error: Missing arguments // This is the only way - complete from birthconst valid = new CompleteAndImmutableService( new PostgresUserRepository(), new RedisCache()); // Cannot modify dependencies - readonly// valid.userRepo = new MockRepo(); // Error: Cannot assign to readonly // ========================================// SETTER INJECTION: Poor completeness & immutability// ======================================== class IncompleteAndMutableService { private userRepo?: UserRepository; private cache?: CacheService; setUserRepository(repo: UserRepository): void { this.userRepo = repo; } setCacheService(cache: CacheService): void { this.cache = cache; } async getUser(id: string): Promise<User | null> { // Must check or assert - completeness not guaranteed if (!this.userRepo || !this.cache) { throw new Error('Service not fully configured'); } const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; const user = await this.userRepo.findById(id); if (user) await this.cache.set(`user:${id}`, user); return user; }} // Compiles but incomplete - runtime error waiting to happenconst incomplete = new IncompleteAndMutableService();incomplete.setUserRepository(new PostgresUserRepository());// Forgot to set cache - will fail at runtime // Dependencies can be swapped - mutableconst mutableService = new IncompleteAndMutableService();mutableService.setUserRepository(new PostgresUserRepository());mutableService.setCacheService(new RedisCache());// Later, some other code does this:mutableService.setUserRepository(new MockUserRepository()); // Silent replacement! // ========================================// INTERFACE INJECTION: Moderate completeness & immutability// ======================================== interface UserRepositoryInjector { injectUserRepository(repo: UserRepository): void;} interface CacheInjector { injectCache(cache: CacheService): void;} class InterfaceInjectedService implements UserRepositoryInjector, CacheInjector { private userRepo!: UserRepository; private cache!: CacheService; private injectionComplete = false; injectUserRepository(repo: UserRepository): void { if (this.injectionComplete) { throw new Error('Dependencies are immutable after injection complete'); } this.userRepo = repo; } injectCache(cache: CacheService): void { if (this.injectionComplete) { throw new Error('Dependencies are immutable after injection complete'); } this.cache = cache; } // Container calls this after all injections markInjectionComplete(): void { if (!this.userRepo || !this.cache) { throw new Error('Not all dependencies were injected'); } this.injectionComplete = true; Object.freeze(this); // Enforce immutability } async getUser(id: string): Promise<User | null> { if (!this.injectionComplete) { throw new Error('Service not fully initialized'); } // Safe to use dependencies const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; const user = await this.userRepo.findById(id); if (user) await this.cache.set(`user:${id}`, user); return user; }} // With a well-implemented container:class InjectionContainer { wire(target: object): void { if (this.isUserRepositoryInjector(target)) { target.injectUserRepository(this.resolveUserRepository()); } if (this.isCacheInjector(target)) { target.injectCache(this.resolveCache()); } // Mark complete after all injections if ('markInjectionComplete' in target) { (target as InterfaceInjectedService).markInjectionComplete(); } } // Type guards and resolution...}The markInjectionComplete() pattern allows Interface Injection to achieve immutability similar to Constructor Injection. The container calls this after all injections, and the object refuses further modifications. Object.freeze() provides runtime enforcement.
A primary motivation for dependency injection is testability. Each injection type has different implications for writing unit tests:
Constructor Injection:
Setter Injection:
Interface Injection:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
// ========================================// TEST SETUP FOR CONSTRUCTOR INJECTION// Cleanest testing experience// ======================================== describe('ConstructorInjectedUserService', () => { let service: ConstructorInjectedUserService; let mockUserRepo: jest.Mocked<UserRepository>; let mockEventPublisher: jest.Mocked<EventPublisher>; let mockLogger: jest.Mocked<Logger>; beforeEach(() => { // Create mocks mockUserRepo = { findById: jest.fn(), save: jest.fn(), }; mockEventPublisher = { publish: jest.fn(), }; mockLogger = { info: jest.fn(), error: jest.fn(), }; // Single clean instantiation with all dependencies service = new ConstructorInjectedUserService( mockUserRepo, mockEventPublisher, mockLogger ); }); it('should update user successfully', async () => { const existingUser = { id: '1', name: 'John', email: 'john@example.com' }; mockUserRepo.findById.mockResolvedValue(existingUser); mockUserRepo.save.mockResolvedValue(); mockEventPublisher.publish.mockResolvedValue(); const result = await service.updateUser('1', { name: 'Jane' }); expect(result.name).toBe('Jane'); expect(mockUserRepo.save).toHaveBeenCalled(); expect(mockEventPublisher.publish).toHaveBeenCalledWith( expect.any(UserUpdatedEvent) ); });}); // ========================================// TEST SETUP FOR SETTER INJECTION// More flexible but risky// ======================================== describe('SetterInjectedUserService', () => { let service: SetterInjectedUserService; let mockUserRepo: jest.Mocked<UserRepository>; let mockEventPublisher: jest.Mocked<EventPublisher>; let mockLogger: jest.Mocked<Logger>; beforeEach(() => { // Create mocks mockUserRepo = { findById: jest.fn(), save: jest.fn(), }; mockEventPublisher = { publish: jest.fn(), }; mockLogger = { info: jest.fn(), error: jest.fn(), }; // Create service and set dependencies service = new SetterInjectedUserService(); service.setUserRepository(mockUserRepo); service.setEventPublisher(mockEventPublisher); service.setLogger(mockLogger); }); it('should update user successfully', async () => { const existingUser = { id: '1', name: 'John', email: 'john@example.com' }; mockUserRepo.findById.mockResolvedValue(existingUser); mockUserRepo.save.mockResolvedValue(); mockEventPublisher.publish.mockResolvedValue(); const result = await service.updateUser('1', { name: 'Jane' }); expect(result.name).toBe('Jane'); }); // Setter injection allows testing partial configuration it('can test with only some dependencies (if the method allows)', async () => { // Create fresh service with only needed dependencies const partialService = new SetterInjectedUserService(); partialService.setLogger(mockLogger); // This would throw because userRepo is not set // But you can test methods that don't need all dependencies // (if such methods exist) }); // Risk: Forgetting to set a dependency it('fails if dependencies are not set', async () => { const incompleteService = new SetterInjectedUserService(); // Forgot to set dependencies await expect(incompleteService.updateUser('1', {})) .rejects.toThrow('Dependencies not configured'); });}); // ========================================// TEST SETUP FOR INTERFACE INJECTION// More setup but mirrors production// ======================================== describe('InterfaceInjectedUserService', () => { let service: InterfaceInjectedUserService; let mockUserRepo: jest.Mocked<UserRepository>; let mockEventPublisher: jest.Mocked<EventPublisher>; let mockLogger: jest.Mocked<Logger>; beforeEach(() => { // Create mocks mockUserRepo = { findById: jest.fn(), save: jest.fn() }; mockEventPublisher = { publish: jest.fn() }; mockLogger = { info: jest.fn(), error: jest.fn() }; // Create service service = new InterfaceInjectedUserService(); // Perform interface injection manually service.injectUserRepository(mockUserRepo); service.injectEventPublisher(mockEventPublisher); service.injectLogger(mockLogger); }); it('should update user successfully', async () => { const existingUser = { id: '1', name: 'John', email: 'john@example.com' }; mockUserRepo.findById.mockResolvedValue(existingUser); mockUserRepo.save.mockResolvedValue(); mockEventPublisher.publish.mockResolvedValue(); const result = await service.updateUser('1', { name: 'Jane' }); expect(result.name).toBe('Jane'); });}); // Alternative: Use a test container for interface injectiondescribe('InterfaceInjectedUserService with TestContainer', () => { let container: TestContainer; beforeEach(() => { container = new TestContainer() .registerMock(UserRepository, { findById: jest.fn(), save: jest.fn() }) .registerMock(EventPublisher, { publish: jest.fn() }) .registerMock(Logger, { info: jest.fn(), error: jest.fn() }); }); it('should update user successfully', async () => { const service = container.create(InterfaceInjectedUserService); container.getMock<UserRepository>(UserRepository).findById .mockResolvedValue({ id: '1', name: 'John', email: 'john@example.com' }); const result = await service.updateUser('1', { name: 'Jane' }); expect(result.name).toBe('Jane'); });}); // ========================================// Test container for interface injection// ======================================== class TestContainer { private mocks: Map<object, object> = new Map(); registerMock<T>(type: new (...args: any[]) => T, mock: Partial<T>): this { this.mocks.set(type, mock); return this; } getMock<T>(type: new (...args: any[]) => T): jest.Mocked<T> { return this.mocks.get(type) as jest.Mocked<T>; } create<T extends object>(ctor: new () => T): T { const instance = new ctor(); // Perform interface injection if ('injectUserRepository' in instance) { (instance as any).injectUserRepository(this.mocks.get(UserRepository)); } if ('injectEventPublisher' in instance) { (instance as any).injectEventPublisher(this.mocks.get(EventPublisher)); } if ('injectLogger' in instance) { (instance as any).injectLogger(this.mocks.get(Logger)); } return instance; }}Constructor Injection provides the cleanest testing experience—mocks passed directly to constructor, no risk of forgotten dependencies. Setter/Interface Injection can be tested but require more careful setup. For this reason, prefer Constructor Injection for application code.
Based on our analysis, here are clear guidelines for choosing the right injection type:
Default to Constructor Injection for:
Choose Setter Injection when:
Choose Interface Injection when:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// ========================================// Decision tree for choosing injection type// ======================================== type InjectionType = 'constructor' | 'setter' | 'interface'; interface InjectionDecisionContext { // Dependency characteristics isRequired: boolean; isImmutable: boolean; isFrameworkService: boolean; // Class characteristics youControlConstruction: boolean; hasLegacyNoArgConstructorRequirement: boolean; isFrameworkExtension: boolean; isPlugin: boolean; // Architecture characteristics needsDiscoveryBasedWiring: boolean; hasCircularDependencies: boolean; dependencyControlsLifecycle: boolean;} function chooseInjectionType(context: InjectionDecisionContext): InjectionType { // Framework extensions and plugins → Interface Injection if (context.isFrameworkExtension || context.isPlugin) { return 'interface'; } // Discovery-based wiring or dependency-controlled lifecycle → Interface if (context.needsDiscoveryBasedWiring || context.dependencyControlsLifecycle) { return 'interface'; } // Circular dependencies or optional dependencies → Setter if (context.hasCircularDependencies || !context.isRequired) { return 'setter'; } // Legacy no-arg constructor requirement → Setter if (context.hasLegacyNoArgConstructorRequirement) { return 'setter'; } // You control construction and want required/immutable → Constructor if (context.youControlConstruction && context.isRequired && context.isImmutable) { return 'constructor'; } // Default to constructor for most application code return 'constructor';} // ========================================// Real-world decision examples// ======================================== // Example 1: Standard application serviceconst orderServiceDecision = chooseInjectionType({ isRequired: true, isImmutable: true, isFrameworkService: false, youControlConstruction: true, hasLegacyNoArgConstructorRequirement: false, isFrameworkExtension: false, isPlugin: false, needsDiscoveryBasedWiring: false, hasCircularDependencies: false, dependencyControlsLifecycle: false,});// Result: 'constructor' // Example 2: Optional email notifierconst emailNotifierDecision = chooseInjectionType({ isRequired: false, // Optional! isImmutable: false, isFrameworkService: false, youControlConstruction: true, hasLegacyNoArgConstructorRequirement: false, isFrameworkExtension: false, isPlugin: false, needsDiscoveryBasedWiring: false, hasCircularDependencies: false, dependencyControlsLifecycle: false,});// Result: 'setter' // Example 3: Framework pluginconst pluginDecision = chooseInjectionType({ isRequired: true, isImmutable: true, isFrameworkService: true, youControlConstruction: false, hasLegacyNoArgConstructorRequirement: false, isFrameworkExtension: true, isPlugin: true, needsDiscoveryBasedWiring: true, hasCircularDependencies: false, dependencyControlsLifecycle: true,});// Result: 'interface' // Example 4: Hibernate entity (no-arg constructor required)const entityDecision = chooseInjectionType({ isRequired: true, isImmutable: false, isFrameworkService: false, youControlConstruction: false, hasLegacyNoArgConstructorRequirement: true, // ORM requirement! isFrameworkExtension: false, isPlugin: false, needsDiscoveryBasedWiring: false, hasCircularDependencies: false, dependencyControlsLifecycle: false,});// Result: 'setter'| Scenario | Recommended Injection | Reason |
|---|---|---|
| Application service with required dependencies | Constructor | Type-safe, immutable, testable |
| Service with optional cache | Setter (for cache only) | Optional dependency pattern |
| Web framework middleware | Interface | Framework extension point |
| Editor/IDE plugin | Interface | Plugin architecture |
| JPA/Hibernate entity | Setter | No-arg constructor required |
| Service A and B depend on each other | Setter | Break circular dependency |
| Cross-cutting concern (Logger, Metrics) | Interface (if framework) or Constructor | Depends on architecture |
In practice, classes often use multiple injection types for different categories of dependencies. This is not only acceptable but often the best approach.
Common hybrid patterns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
// ========================================// Pattern 1: Constructor + Setter// Required via constructor, optional via setter// ======================================== class HybridPaymentService { // Required dependencies - constructor injected constructor( private readonly paymentGateway: PaymentGateway, private readonly transactionLog: TransactionLog ) {} // Optional dependencies - setter injected private fraudChecker?: FraudChecker; private notifier?: NotificationService; setFraudChecker(checker: FraudChecker): void { this.fraudChecker = checker; } setNotifier(notifier: NotificationService): void { this.notifier = notifier; } async processPayment(payment: Payment): Promise<PaymentResult> { // Required dependencies always available await this.transactionLog.begin(payment); // Optional fraud check if configured if (this.fraudChecker) { const fraudRisk = await this.fraudChecker.assess(payment); if (fraudRisk > 0.8) { await this.transactionLog.flag(payment, 'FRAUD_SUSPECTED'); throw new FraudDetectedError(); } } const result = await this.paymentGateway.charge(payment); await this.transactionLog.commit(payment, result); // Optional notification if configured if (this.notifier) { await this.notifier.sendPaymentConfirmation(payment, result); } return result; }} // Usageconst service = new HybridPaymentService( new StripeGateway(), new PostgresTransactionLog());// Optional: add fraud checking in productionif (process.env.NODE_ENV === 'production') { service.setFraudChecker(new MLFraudChecker());}service.setNotifier(new EmailNotifier()); // ========================================// Pattern 2: Constructor + Interface// Core deps via constructor, framework services via interface// ======================================== interface LoggerAware { setLogger(logger: Logger): void;} interface MetricsAware { setMetrics(metrics: MetricsCollector): void;} class FrameworkIntegratedOrderService implements LoggerAware, MetricsAware { // Core business dependencies - constructor injected constructor( private readonly orderRepo: OrderRepository, private readonly inventoryService: InventoryService, private readonly pricingEngine: PricingEngine ) {} // Framework/infrastructure deps - interface injected private logger?: Logger; private metrics?: MetricsCollector; setLogger(logger: Logger): void { this.logger = logger; } setMetrics(metrics: MetricsCollector): void { this.metrics = metrics; } async createOrder(request: OrderRequest): Promise<Order> { this.logger?.info('Creating order', { customerId: request.customerId }); const startTime = Date.now(); try { // Core business logic uses constructor-injected dependencies const items = await this.inventoryService.reserveItems(request.items); const pricing = await this.pricingEngine.calculate(items, request.discounts); const order = await this.orderRepo.create({ customerId: request.customerId, items, total: pricing.total, }); this.metrics?.incrementCounter('orders.created'); this.metrics?.recordHistogram('orders.duration_ms', Date.now() - startTime); this.logger?.info('Order created', { orderId: order.id }); return order; } catch (error) { this.metrics?.incrementCounter('orders.failed'); this.logger?.error('Order creation failed', error as Error); throw error; } }} // Usage: Constructor injection for business depsconst orderService = new FrameworkIntegratedOrderService( new PostgresOrderRepository(), new InventoryService(), new DynamicPricingEngine()); // Framework container adds infrastructure via interface injectionframeworkContainer.wire(orderService); // ========================================// Pattern 3: Interface + Lifecycle// Interface injection with lifecycle callbacks// ======================================== interface Initializable { initialize(): Promise<void>;} interface Destroyable { destroy(): Promise<void>;} class DatabaseQueueProcessor implements DatabaseConnectionInjector, MessageQueueInjector, LoggerInjector, Initializable, Destroyable { private db!: DatabaseConnection; private queue!: MessageQueue; private logger!: Logger; private processing = false; // Interface injection for all dependencies injectDatabaseConnection(db: DatabaseConnection): void { this.db = db; } injectMessageQueue(queue: MessageQueue): void { this.queue = queue; } injectLogger(logger: Logger): void { this.logger = logger; } // Lifecycle: called after all injections async initialize(): Promise<void> { this.logger.info('Initializing queue processor'); await this.db.connect(); await this.queue.subscribe('orders', this.processMessage.bind(this)); this.processing = true; this.logger.info('Queue processor initialized and listening'); } // Lifecycle: called before shutdown async destroy(): Promise<void> { this.logger.info('Shutting down queue processor'); this.processing = false; await this.queue.unsubscribe('orders'); await this.db.disconnect(); this.logger.info('Queue processor shut down'); } private async processMessage(message: Message): Promise<void> { if (!this.processing) return; this.logger.info('Processing message', { messageId: message.id }); // Process with database... }} // Container handles injection + lifecycleclass LifecycleContainer { async create<T extends object>(ctor: new () => T): Promise<T> { const instance = new ctor(); // Perform interface injection this.injectDependencies(instance); // Call lifecycle hook if (this.isInitializable(instance)) { await instance.initialize(); } return instance; } async destroy<T extends object>(instance: T): Promise<void> { if (this.isDestroyable(instance)) { await instance.destroy(); } } // ... injection and type guard methods}A common and effective pattern is: Constructor injection for business dependencies (repositories, domain services) + Interface injection for infrastructure concerns (logging, metrics, tracing). This keeps core logic clean while allowing framework integration.
We've comprehensively compared the three dependency injection mechanisms. Each has its place in a well-designed system, and understanding their tradeoffs enables you to make informed architectural decisions.
| Injection Type | Best For | Avoid When |
|---|---|---|
| Constructor | Required dependencies, application services, testability focus | Optional dependencies, circular dependencies, legacy no-arg requirements |
| Setter | Optional dependencies, legacy integration, breaking cycles | Required dependencies where you want compiler enforcement |
| Interface | Frameworks, plugins, discovery-based wiring, lifecycle-aware injection | Simple application code where constructor would suffice |
You've now mastered Interface Injection—from the fundamental concept of injector interfaces, through implementation patterns and use cases, to detailed comparison with other injection types. You understand when each approach excels and can make informed architectural decisions about dependency injection in any context.