Loading learning content...
Among the three injection patterns—Constructor, Setter, and Interface—Interface Injection is the least commonly used and least discussed. Yet understanding it completes your dependency injection vocabulary and helps you recognize it when you encounter it in frameworks and legacy systems.
With Interface Injection, the dependent class implements an interface that declares a method for receiving the dependency. The injector knows that any class implementing that interface can receive the dependency through that method. The interface itself becomes the contract for injection.
This pattern inverts the usual relationship: instead of the class deciding how to receive dependencies, an interface dictates the injection mechanism.
By the end of this page, you will understand Interface Injection mechanics, recognize where it appears in practice (especially in frameworks), compare it to Constructor and Setter Injection, and know when—and when not—to use this pattern in your designs.
Interface Injection follows this pattern:
Define an injection interface — An interface declares a method for receiving a specific dependency type.
Dependent class implements the interface — By implementing the interface, the class signals that it needs that dependency and knows how to receive it.
Injector checks for interface implementation — The injector/framework detects that the class implements the injection interface.
Injector calls the injection method — The framework invokes the interface method, passing the dependency.
The key distinction: the interface defines how injection happens, not the class's arbitrary setter or constructor.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// ANATOMY OF INTERFACE INJECTION // Step 1: Define injection interfaces// Each interface declares a method for receiving a specific dependencyinterface LoggerAware { setLogger(logger: Logger): void;} interface RepositoryAware { setRepository(repository: Repository): void;} interface EventPublisherAware { setEventPublisher(publisher: EventPublisher): void;} // Step 2: Dependent class implements relevant interfaces// The class signals which dependencies it needs by implementing interfacesclass OrderService implements LoggerAware, RepositoryAware { private logger!: Logger; private repository!: Repository; // Interface-mandated injection methods setLogger(logger: Logger): void { this.logger = logger; } setRepository(repository: Repository): void { this.repository = repository; } async processOrder(order: Order): Promise<void> { this.logger.info(`Processing order ${order.id}`); await this.repository.save(order); }} // Step 3 & 4: Injector/framework detects interfaces and calls methodsfunction injectDependencies(target: unknown, container: Container): void { // Check for LoggerAware interface if (isLoggerAware(target)) { target.setLogger(container.get<Logger>("Logger")); } // Check for RepositoryAware interface if (isRepositoryAware(target)) { target.setRepository(container.get<Repository>("Repository")); } // Check for EventPublisherAware interface if (isEventPublisherAware(target)) { target.setEventPublisher(container.get<EventPublisher>("EventPublisher")); }} // Type guards for interface detectionfunction isLoggerAware(obj: unknown): obj is LoggerAware { return typeof (obj as LoggerAware).setLogger === "function";} function isRepositoryAware(obj: unknown): obj is RepositoryAware { return typeof (obj as RepositoryAware).setRepository === "function";}You'll often see interface injection interfaces named with the *Aware suffix: ApplicationContextAware, LoggerAware, EnvironmentAware. This convention comes from Spring Framework and signals that implementing the interface grants awareness of (access to) that dependency.
At first glance, Interface Injection looks identical to Setter Injection—both use setter methods to provide dependencies. The crucial difference lies in who defines the setter method:
Setter Injection: The class defines its own setter methods. The setter signature is arbitrary—the class controls it.
Interface Injection: An interface defines the setter method. The class implements the interface and must conform to its signature. The framework/injector looks for interface implementations, not arbitrary methods.
| Aspect | Setter Injection | Interface Injection |
|---|---|---|
| Who defines the setter | The dependent class | An interface (shared contract) |
| Method signature | Arbitrary (class decides) | Standardized (interface mandates) |
| Detection by injector | By method name/annotation | By interface implementation |
| Coupling | Class coupled to setter convention | Class coupled to interface type |
| Consistency | Each class may differ | All implementers use same method |
| Framework integration | Annotations or naming conventions | Type-safe interface checking |
12345678910111213141516171819202122232425
// SETTER INJECTION// Class defines its own method class OrderService { private log?: Logger; // Arbitrary name decided by class setLog(logger: Logger) { this.log = logger; }} class UserService { private logging?: Logger; // Different name, same purpose setLogging(logger: Logger) { this.logging = logger; }} // Injector needs to know each// class's specific setter nameinject(orderService, "setLog", logger);inject(userService, "setLogging", logger);1234567891011121314151617181920212223242526272829
// INTERFACE INJECTION// Interface defines the method interface LoggerAware { setLogger(logger: Logger): void;} class OrderService implements LoggerAware { private logger?: Logger; // Must match interface signature setLogger(logger: Logger) { this.logger = logger; }} class UserService implements LoggerAware { private logger?: Logger; // Same method, same name setLogger(logger: Logger) { this.logger = logger; }} // Injector uses interface checkif (isLoggerAware(service)) { service.setLogger(logger);}The standardization benefit:
Interface Injection provides consistency across a codebase or framework. Every class that needs a Logger implements LoggerAware and receives it through setLogger(). The injector doesn't need to know anything about the class's internals—just check if it implements the interface and call the standardized method.
This is why frameworks use Interface Injection: they can inject framework-provided services into user classes without knowing anything about those classes beyond which interfaces they implement.
Interface Injection appears prominently in frameworks that need to inject framework services into user-defined classes. The most notable example is *Spring Framework's Aware interfaces.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// SPRING FRAMEWORK *AWARE INTERFACES // ApplicationContextAware - receive the Spring containerpublic interface ApplicationContextAware { void setApplicationContext(ApplicationContext context);} // BeanNameAware - receive the bean's name in the containerpublic interface BeanNameAware { void setBeanName(String name);} // EnvironmentAware - receive environment configurationpublic interface EnvironmentAware { void setEnvironment(Environment environment);} // ResourceLoaderAware - receive resource loading capabilitypublic interface ResourceLoaderAware { void setResourceLoader(ResourceLoader loader);} // Example: A service using Interface Injection@Servicepublic class DynamicPluginLoader implements ApplicationContextAware, EnvironmentAware { private ApplicationContext context; private Environment environment; // Spring calls this automatically during initialization @Override public void setApplicationContext(ApplicationContext context) { this.context = context; } // Spring calls this automatically during initialization @Override public void setEnvironment(Environment environment) { this.environment = environment; } public void loadPlugin(String pluginName) { // Use context to dynamically lookup beans String pluginClass = environment.getProperty("plugin." + pluginName); Plugin plugin = context.getBean(pluginClass, Plugin.class); plugin.initialize(); }}Servlet Lifecycle Callbacks:
Java Servlet specification uses a similar pattern. Servlets implement interfaces to receive container-provided resources:
1234567891011121314151617181920212223242526272829303132
// SERVLET SPECIFICATION INTERFACE INJECTION // ServletContextAware equivalentpublic interface ServletContextListener { void contextInitialized(ServletContextEvent event); void contextDestroyed(ServletContextEvent event);} // HttpSessionAware equivalentpublic interface HttpSessionListener { void sessionCreated(HttpSessionEvent event); void sessionDestroyed(HttpSessionEvent event);} // Example servlet receiving the ServletConfigpublic class MyServlet extends HttpServlet { private ServletConfig config; // Container calls this during servlet initialization @Override public void init(ServletConfig config) throws ServletException { this.config = config; // Access context, parameters, etc. String dbUrl = config.getInitParameter("database.url"); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { // Use config... }}.NET's IServiceProvider Pattern:
In .NET, interface injection appears in hosting and middleware contexts:
12345678910111213141516171819202122232425262728293031323334353637383940
// .NET HOSTED SERVICE INTERFACE INJECTION // IHostedService - for long-running background servicespublic interface IHostedService{ Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken);} // Implementing class receives lifecycle callbackspublic class BackgroundEmailSender : IHostedService{ private readonly IEmailQueue _queue; private Timer _timer; // Constructor injection for business dependencies public BackgroundEmailSender(IEmailQueue queue) { _queue = queue; } // Interface injection: host calls this to start the service public Task StartAsync(CancellationToken cancellationToken) { _timer = new Timer(ProcessQueue, null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); return Task.CompletedTask; } // Interface injection: host calls this during shutdown public Task StopAsync(CancellationToken cancellationToken) { _timer?.Dispose(); return Task.CompletedTask; } private void ProcessQueue(object state) { // Process pending emails }}Interface Injection is most common at framework integration points—where your code receives framework-provided capabilities. When you need to access the application context, environment, lifecycle events, or container services, you'll often see interfaces like *Aware, *Listener, or *Callback.
When Interface Injection makes sense:
Framework-provided services — The framework controls what's injected (e.g., ApplicationContext, Environment). You don't choose the implementation; you just receive it.
Cross-cutting concerns — Logging, configuration, and monitoring that many classes might need. A single LoggerAware interface serves the entire codebase.
Plugin systems — Plugins implement interfaces to receive host-provided capabilities without knowing the host's concrete types.
Lifecycle callbacks — When the container/framework needs to notify objects of lifecycle events (initialization, shutdown).
When to avoid Interface Injection:
Business dependencies — For domain services depending on other domain services, Constructor Injection is clearer and enforces completeness.
Many dependencies — If a class needs 5 interfaces, it becomes cluttered: class Service implements A, B, C, D, E. Constructor parameters are more compact.
Required dependencies — Interface Injection doesn't guarantee dependencies are provided before use. Constructor Injection does.
Application code — Interface Injection is primarily a framework pattern. In application code, prefer Constructor/Setter Injection.
While Interface Injection is most common in frameworks, you can implement it in your own codebase for cross-cutting concerns. Here's a complete implementation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
// IMPLEMENTING INTERFACE INJECTION // Step 1: Define injection interfaces for cross-cutting concernsinterface LoggerInjectable { injectLogger(logger: Logger): void;} interface ConfigInjectable { injectConfig(config: AppConfig): void;} interface MetricsInjectable { injectMetrics(metrics: MetricsCollector): void;} // Type guards for runtime detectionfunction isLoggerInjectable(obj: unknown): obj is LoggerInjectable { return obj !== null && typeof obj === "object" && typeof (obj as LoggerInjectable).injectLogger === "function";} function isConfigInjectable(obj: unknown): obj is ConfigInjectable { return obj !== null && typeof obj === "object" && typeof (obj as ConfigInjectable).injectConfig === "function";} function isMetricsInjectable(obj: unknown): obj is MetricsInjectable { return obj !== null && typeof obj === "object" && typeof (obj as MetricsInjectable).injectMetrics === "function";} // Step 2: Create the injectorclass DependencyInjector { private logger: Logger; private config: AppConfig; private metrics: MetricsCollector; constructor( logger: Logger, config: AppConfig, metrics: MetricsCollector ) { this.logger = logger; this.config = config; this.metrics = metrics; } inject<T>(target: T): T { // Check each interface and inject if implemented if (isLoggerInjectable(target)) { target.injectLogger(this.logger); } if (isConfigInjectable(target)) { target.injectConfig(this.config); } if (isMetricsInjectable(target)) { target.injectMetrics(this.metrics); } return target; } // Create and inject in one step create<T>(Factory: new () => T): T { const instance = new Factory(); return this.inject(instance); }} // Step 3: Classes implement interfaces as neededclass OrderService implements LoggerInjectable, MetricsInjectable { private logger!: Logger; private metrics!: MetricsCollector; private readonly repository: OrderRepository; // Constructor for business dependencies constructor(repository: OrderRepository) { this.repository = repository; } // Interface methods for cross-cutting concerns injectLogger(logger: Logger): void { this.logger = logger; } injectMetrics(metrics: MetricsCollector): void { this.metrics = metrics; } async processOrder(order: Order): Promise<void> { const startTime = Date.now(); this.logger.info(`Processing order ${order.id}`); await this.repository.save(order); this.metrics.recordTiming("order.process", Date.now() - startTime); this.logger.info(`Order ${order.id} processed`); }} // Step 4: Usageconst injector = new DependencyInjector( new WinstonLogger(), loadConfig(), new PrometheusMetrics()); // Create with constructor dependency, then inject cross-cuttingconst orderService = new OrderService(new PostgresOrderRepository());injector.inject(orderService); // Now orderService has logger & metrics injectedThe most practical pattern combines Constructor Injection for business dependencies and Interface Injection for framework/cross-cutting concerns. Business logic receives its collaborators through the constructor; logging, configuration, and monitoring come through interfaces.
Testing classes that use Interface Injection follows a similar pattern to Setter Injection testing. You must ensure injection methods are called before exercising the class:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
describe("OrderService with Interface Injection", () => { let service: OrderService; let mockRepository: jest.Mocked<OrderRepository>; let mockLogger: jest.Mocked<Logger>; let mockMetrics: jest.Mocked<MetricsCollector>; beforeEach(() => { // Create mocks mockRepository = { save: jest.fn(), findById: jest.fn() }; mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn() }; mockMetrics = { recordTiming: jest.fn(), incrementCounter: jest.fn() }; // Create service with constructor dependency service = new OrderService(mockRepository); // Call interface injection methods service.injectLogger(mockLogger); service.injectMetrics(mockMetrics); }); describe("processOrder", () => { it("should log and record metrics", async () => { const order = createTestOrder({ id: "ORD-123" }); mockRepository.save.mockResolvedValue(undefined); await service.processOrder(order); expect(mockLogger.info).toHaveBeenCalledWith( "Processing order ORD-123" ); expect(mockMetrics.recordTiming).toHaveBeenCalledWith( "order.process", expect.any(Number) ); }); }); describe("without logger injected", () => { it("should throw if logger not injected", async () => { // Create new service without injecting logger const uninitializedService = new OrderService(mockRepository); // Don't call injectLogger await expect(uninitializedService.processOrder(createTestOrder())) .rejects.toThrow(); }); });}); // Helper to create fully injected servicefunction createFullyInjectedService( repository: OrderRepository): OrderService { const service = new OrderService(repository); service.injectLogger(createMockLogger()); service.injectMetrics(createMockMetrics()); return service;}Using an injector in tests:
Alternatively, create a test injector that provides mock implementations:
123456789101112131415161718192021222324252627282930313233343536
// Create a test injector with mocksfunction createTestInjector() { const mockLogger = createMockLogger(); const mockConfig = createMockConfig(); const mockMetrics = createMockMetrics(); const injector = new DependencyInjector( mockLogger, mockConfig, mockMetrics ); return { injector, mocks: { logger: mockLogger, config: mockConfig, metrics: mockMetrics } };} // In testsdescribe("OrderService via injector", () => { it("should use injected mocks", async () => { const { injector, mocks } = createTestInjector(); const service = injector.inject( new OrderService(createMockRepository()) ); await service.processOrder(createTestOrder()); expect(mocks.logger.info).toHaveBeenCalled(); });});With all three injection patterns explored, let's consolidate the comparison:
| Criterion | Constructor | Setter | Interface |
|---|---|---|---|
| Object readiness | Immediate | After setters called | After interface methods called |
| Dependency visibility | Explicit in constructor | Scattered setters | Scattered interfaces |
| Immutability | Easy (readonly fields) | Requires discipline | Requires discipline |
| Required dependencies | Perfect fit | Needs validation | Needs validation |
| Optional dependencies | Possible with defaults | Natural fit | Less common |
| Testing | Simple - pass mocks | Remember to call setters | Remember to call injection methods |
| Framework detection | Analyze constructor | Naming/annotations | Interface implementation |
| Primary use case | Business dependencies | Optional/late deps | Framework services |
Use Constructor Injection for required business dependencies that the class cannot function without. Use Setter Injection for optional dependencies that enhance but aren't essential. Use Interface Injection when implementing framework contracts or standardizing cross-cutting concern injection across many classes.
Interface Injection completes your dependency injection vocabulary. Let's consolidate the essential knowledge:
What's next:
The final page of this module addresses the crucial decision: Choosing the Right Injection Style. You'll develop a decision framework for selecting among Constructor, Setter, and Interface Injection based on specific requirements, constraints, and trade-offs. This practical guidance completes your Dependency Injection fundamentals.
You now understand all three injection patterns: Constructor, Setter, and Interface. Each has its place—Constructor for the common case, Setter for optional dependencies, Interface for framework integration. The next page provides the framework for choosing among them in real-world scenarios.