Loading learning content...
Interface Injection isn't the most common form of dependency injection—Constructor Injection typically wins for day-to-day application code. But Interface Injection has specific domains where it provides unique advantages that other injection types cannot match.
Understanding these use cases helps you recognize when to reach for Interface Injection and when to stick with simpler alternatives. The goal isn't to use Interface Injection everywhere, but to use it where it provides genuine architectural value.
This page explores real-world scenarios where Interface Injection shines: framework extension points, plugin architectures, lifecycle-aware injection, and cross-cutting concerns.
By the end of this page, you will understand the specific scenarios where Interface Injection provides unique advantages. You'll see complete implementations for plugin architectures, framework extensions, lifecycle callbacks, and cross-cutting concerns—enabling you to apply Interface Injection effectively in your own systems.
The most compelling use case for Interface Injection is framework extension points. When building a framework, you cannot know what classes users will create. But you can define injector interfaces that user classes implement to receive framework services.
Why Interface Injection wins here:
Let's build a complete example: a web framework with extensible middleware and handlers.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
// ========================================// FRAMEWORK CORE: Defines injector interfaces// ======================================== namespace WebFramework { // Core types export interface HttpRequest { method: string; path: string; headers: Record<string, string>; body: unknown; params: Record<string, string>; query: Record<string, string>; } export interface HttpResponse { status(code: number): HttpResponse; json(data: unknown): void; send(body: string): void; header(name: string, value: string): HttpResponse; } export interface RequestContext { request: HttpRequest; response: HttpResponse; state: Map<string, unknown>; } // Framework services available for injection export interface Logger { debug(message: string): void; info(message: string): void; warn(message: string): void; error(message: string, error?: Error): void; } export interface Configuration { get<T>(key: string): T | undefined; getRequired<T>(key: string): T; has(key: string): boolean; } export interface RouteRegistry { register(method: string, path: string, handler: RouteHandler): void; } export interface MiddlewareRegistry { use(middleware: Middleware): void; useFor(pathPattern: string, middleware: Middleware): void; } export type RouteHandler = (ctx: RequestContext) => Promise<void>; export type Middleware = (ctx: RequestContext, next: () => Promise<void>) => Promise<void>; // ======================================== // INJECTOR INTERFACES // User classes implement these to receive services // ======================================== export interface LoggerAware { setLogger(logger: Logger): void; } export interface ConfigurationAware { setConfiguration(config: Configuration): void; } export interface RouteRegistryAware { setRouteRegistry(registry: RouteRegistry): void; } export interface MiddlewareRegistryAware { setMiddlewareRegistry(registry: MiddlewareRegistry): void; } export interface ContextAware { setContext(context: RequestContext): void; } // Detection helpers export function isLoggerAware(obj: unknown): obj is LoggerAware { return typeof (obj as LoggerAware)?.setLogger === 'function'; } export function isConfigurationAware(obj: unknown): obj is ConfigurationAware { return typeof (obj as ConfigurationAware)?.setConfiguration === 'function'; } export function isRouteRegistryAware(obj: unknown): obj is RouteRegistryAware { return typeof (obj as RouteRegistryAware)?.setRouteRegistry === 'function'; } export function isMiddlewareRegistryAware(obj: unknown): obj is MiddlewareRegistryAware { return typeof (obj as MiddlewareRegistryAware)?.setMiddlewareRegistry === 'function'; } export function isContextAware(obj: unknown): obj is ContextAware { return typeof (obj as ContextAware)?.setContext === 'function'; }} // ========================================// FRAMEWORK: Extension loader with injection// ======================================== class FrameworkApp { private logger: WebFramework.Logger; private config: WebFramework.Configuration; private routeRegistry: WebFramework.RouteRegistry; private middlewareRegistry: WebFramework.MiddlewareRegistry; private extensions: object[] = []; constructor(configPath: string) { this.config = Configuration.load(configPath); this.logger = new FrameworkLogger(this.config); this.routeRegistry = new DefaultRouteRegistry(); this.middlewareRegistry = new DefaultMiddlewareRegistry(); } /** * Load an extension (plugin, controller, middleware class) * and inject framework services based on implemented interfaces */ loadExtension(extension: object): this { console.log(`[Framework] Loading extension: ${extension.constructor.name}`); this.performInterfaceInjection(extension); this.extensions.push(extension); return this; } /** * Detect and perform interface injection */ private performInterfaceInjection(target: object): void { // Inject Logger if LoggerAware if (WebFramework.isLoggerAware(target)) { console.log(`[Framework] Injecting Logger into ${target.constructor.name}`); target.setLogger(this.logger); } // Inject Configuration if ConfigurationAware if (WebFramework.isConfigurationAware(target)) { console.log(`[Framework] Injecting Configuration into ${target.constructor.name}`); target.setConfiguration(this.config); } // Inject RouteRegistry if RouteRegistryAware if (WebFramework.isRouteRegistryAware(target)) { console.log(`[Framework] Injecting RouteRegistry into ${target.constructor.name}`); target.setRouteRegistry(this.routeRegistry); } // Inject MiddlewareRegistry if MiddlewareRegistryAware if (WebFramework.isMiddlewareRegistryAware(target)) { console.log(`[Framework] Injecting MiddlewareRegistry into ${target.constructor.name}`); target.setMiddlewareRegistry(this.middlewareRegistry); } } start(port: number): void { console.log(`[Framework] Starting on port ${port}`); // Start HTTP server... }} // ========================================// USER CODE: Extensions implementing injector interfaces// ======================================== /** * User-defined controller * Framework injects services based on implemented interfaces */class UserController implements WebFramework.LoggerAware, WebFramework.RouteRegistryAware { private logger!: WebFramework.Logger; private routes!: WebFramework.RouteRegistry; // Receive logger from framework setLogger(logger: WebFramework.Logger): void { this.logger = logger; this.logger.info('[UserController] Logger injected'); } // Receive route registry and register routes setRouteRegistry(registry: WebFramework.RouteRegistry): void { this.routes = registry; this.registerRoutes(); } private registerRoutes(): void { this.routes.register('GET', '/users', this.listUsers.bind(this)); this.routes.register('GET', '/users/:id', this.getUser.bind(this)); this.routes.register('POST', '/users', this.createUser.bind(this)); this.logger.info('[UserController] Routes registered'); } async listUsers(ctx: WebFramework.RequestContext): Promise<void> { this.logger.info('Listing users'); ctx.response.json({ users: [] }); } async getUser(ctx: WebFramework.RequestContext): Promise<void> { const id = ctx.request.params.id; this.logger.info(`Getting user ${id}`); ctx.response.json({ id, name: 'Example User' }); } async createUser(ctx: WebFramework.RequestContext): Promise<void> { this.logger.info('Creating user'); ctx.response.status(201).json({ id: 'new-id', ...ctx.request.body }); }} /** * User-defined authentication middleware */class AuthMiddleware implements WebFramework.LoggerAware, WebFramework.ConfigurationAware, WebFramework.MiddlewareRegistryAware { private logger!: WebFramework.Logger; private config!: WebFramework.Configuration; private jwtSecret!: string; setLogger(logger: WebFramework.Logger): void { this.logger = logger; } setConfiguration(config: WebFramework.Configuration): void { this.config = config; this.jwtSecret = config.getRequired<string>('auth.jwtSecret'); } setMiddlewareRegistry(registry: WebFramework.MiddlewareRegistry): void { // Register for all protected paths registry.useFor('/api/*', this.authenticate.bind(this)); this.logger.info('[AuthMiddleware] Registered for /api/*'); } async authenticate( ctx: WebFramework.RequestContext, next: () => Promise<void> ): Promise<void> { const token = ctx.request.headers['authorization']; if (!token) { this.logger.warn('Missing authorization header'); ctx.response.status(401).json({ error: 'Unauthorized' }); return; } try { const payload = this.verifyToken(token); ctx.state.set('user', payload); await next(); } catch (error) { this.logger.error('Token verification failed', error as Error); ctx.response.status(401).json({ error: 'Invalid token' }); } } private verifyToken(token: string): unknown { // JWT verification logic... return { userId: '123' }; }} // ========================================// APPLICATION BOOTSTRAP// ======================================== const app = new FrameworkApp('./config.json'); // Load user extensions - framework injects based on interfacesapp.loadExtension(new UserController());app.loadExtension(new AuthMiddleware()); app.start(3000);Notice how the framework never imports UserController or AuthMiddleware. The user creates them and passes them to loadExtension(). The framework discovers what services to inject by checking which interfaces each extension implements. This enables true plugin-style extensibility.
Plugin systems take framework extensions further. Plugins are independently developed components that can be dynamically loaded at runtime. Interface Injection is essential here because:
Let's build a plugin system for a text editor.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
// ========================================// EDITOR CORE: Plugin API with injector interfaces// ======================================== namespace EditorPlugin { // Plugin metadata export interface PluginManifest { id: string; name: string; version: string; description: string; author: string; activationEvents?: string[]; } // Core editor services export interface DocumentService { getActiveDocument(): Document | undefined; openDocument(path: string): Promise<Document>; createDocument(content: string): Document; } export interface Document { readonly id: string; readonly path: string; readonly content: string; readonly language: string; insertText(position: Position, text: string): void; deleteRange(start: Position, end: Position): void; getText(range?: Range): string; } export interface CommandRegistry { registerCommand(id: string, handler: CommandHandler): Disposable; executeCommand(id: string, ...args: unknown[]): Promise<unknown>; } export interface MenuRegistry { registerMenuItem(location: MenuLocation, item: MenuItem): Disposable; } export interface StatusBar { addItem(item: StatusBarItem): Disposable; } export interface Notifications { showInfo(message: string): void; showWarning(message: string): void; showError(message: string): void; } export interface Storage { get<T>(key: string): T | undefined; set<T>(key: string, value: T): Promise<void>; delete(key: string): Promise<void>; } // Disposable pattern for cleanup export interface Disposable { dispose(): void; } // ======================================== // INJECTOR INTERFACES FOR PLUGINS // ======================================== export interface DocumentServiceInjector { injectDocumentService(service: DocumentService): void; } export interface CommandRegistryInjector { injectCommandRegistry(registry: CommandRegistry): void; } export interface MenuRegistryInjector { injectMenuRegistry(registry: MenuRegistry): void; } export interface StatusBarInjector { injectStatusBar(statusBar: StatusBar): void; } export interface NotificationsInjector { injectNotifications(notifications: Notifications): void; } export interface StorageInjector { injectStorage(storage: Storage): void; } // Plugin lifecycle interface export interface Plugin { readonly manifest: PluginManifest; activate(): void | Promise<void>; deactivate(): void | Promise<void>; }} // ========================================// EDITOR: Plugin loader with interface injection// ======================================== class PluginLoader { private documentService: EditorPlugin.DocumentService; private commandRegistry: EditorPlugin.CommandRegistry; private menuRegistry: EditorPlugin.MenuRegistry; private statusBar: EditorPlugin.StatusBar; private notifications: EditorPlugin.Notifications; private loadedPlugins: Map<string, EditorPlugin.Plugin> = new Map(); private pluginStorage: Map<string, EditorPlugin.Storage> = new Map(); constructor(editorContext: EditorContext) { this.documentService = editorContext.documents; this.commandRegistry = editorContext.commands; this.menuRegistry = editorContext.menus; this.statusBar = editorContext.statusBar; this.notifications = editorContext.notifications; } /** * Load and activate a plugin */ async loadPlugin(plugin: EditorPlugin.Plugin): Promise<void> { const { id, name, version } = plugin.manifest; console.log(`[Editor] Loading plugin: ${name} v${version}`); // Perform interface injection before activation this.injectServices(plugin, id); // Activate the plugin await plugin.activate(); this.loadedPlugins.set(id, plugin); console.log(`[Editor] Plugin activated: ${name}`); } /** * Detect and inject services based on implemented interfaces */ private injectServices(plugin: object, pluginId: string): void { // DocumentService injection if (this.needsDocumentService(plugin)) { console.log(`[Editor] Injecting DocumentService into ${pluginId}`); plugin.injectDocumentService(this.documentService); } // CommandRegistry injection if (this.needsCommandRegistry(plugin)) { console.log(`[Editor] Injecting CommandRegistry into ${pluginId}`); plugin.injectCommandRegistry(this.commandRegistry); } // MenuRegistry injection if (this.needsMenuRegistry(plugin)) { console.log(`[Editor] Injecting MenuRegistry into ${pluginId}`); plugin.injectMenuRegistry(this.menuRegistry); } // StatusBar injection if (this.needsStatusBar(plugin)) { console.log(`[Editor] Injecting StatusBar into ${pluginId}`); plugin.injectStatusBar(this.statusBar); } // Notifications injection if (this.needsNotifications(plugin)) { console.log(`[Editor] Injecting Notifications into ${pluginId}`); plugin.injectNotifications(this.notifications); } // Storage injection (each plugin gets isolated storage) if (this.needsStorage(plugin)) { const storage = this.getOrCreatePluginStorage(pluginId); console.log(`[Editor] Injecting isolated Storage into ${pluginId}`); plugin.injectStorage(storage); } } private getOrCreatePluginStorage(pluginId: string): EditorPlugin.Storage { if (!this.pluginStorage.has(pluginId)) { this.pluginStorage.set(pluginId, new IsolatedPluginStorage(pluginId)); } return this.pluginStorage.get(pluginId)!; } async unloadPlugin(pluginId: string): Promise<void> { const plugin = this.loadedPlugins.get(pluginId); if (!plugin) return; console.log(`[Editor] Unloading plugin: ${plugin.manifest.name}`); await plugin.deactivate(); this.loadedPlugins.delete(pluginId); } // Type guards private needsDocumentService(obj: unknown): obj is EditorPlugin.DocumentServiceInjector { return typeof (obj as EditorPlugin.DocumentServiceInjector)?.injectDocumentService === 'function'; } private needsCommandRegistry(obj: unknown): obj is EditorPlugin.CommandRegistryInjector { return typeof (obj as EditorPlugin.CommandRegistryInjector)?.injectCommandRegistry === 'function'; } private needsMenuRegistry(obj: unknown): obj is EditorPlugin.MenuRegistryInjector { return typeof (obj as EditorPlugin.MenuRegistryInjector)?.injectMenuRegistry === 'function'; } private needsStatusBar(obj: unknown): obj is EditorPlugin.StatusBarInjector { return typeof (obj as EditorPlugin.StatusBarInjector)?.injectStatusBar === 'function'; } private needsNotifications(obj: unknown): obj is EditorPlugin.NotificationsInjector { return typeof (obj as EditorPlugin.NotificationsInjector)?.injectNotifications === 'function'; } private needsStorage(obj: unknown): obj is EditorPlugin.StorageInjector { return typeof (obj as EditorPlugin.StorageInjector)?.injectStorage === 'function'; }} // ========================================// PLUGIN EXAMPLE: Word Count Plugin// ======================================== class WordCountPlugin implements EditorPlugin.Plugin, EditorPlugin.DocumentServiceInjector, EditorPlugin.CommandRegistryInjector, EditorPlugin.StatusBarInjector, EditorPlugin.NotificationsInjector { readonly manifest: EditorPlugin.PluginManifest = { id: 'word-count', name: 'Word Count', version: '1.0.0', description: 'Display word count for the active document', author: 'Plugin Developer', }; private documents!: EditorPlugin.DocumentService; private commands!: EditorPlugin.CommandRegistry; private statusBar!: EditorPlugin.StatusBar; private notifications!: EditorPlugin.Notifications; private disposables: EditorPlugin.Disposable[] = []; // Interface injection methods injectDocumentService(service: EditorPlugin.DocumentService): void { this.documents = service; } injectCommandRegistry(registry: EditorPlugin.CommandRegistry): void { this.commands = registry; } injectStatusBar(statusBar: EditorPlugin.StatusBar): void { this.statusBar = statusBar; } injectNotifications(notifications: EditorPlugin.Notifications): void { this.notifications = notifications; } // Lifecycle activate(): void { console.log('[WordCountPlugin] Activating...'); // Register command const cmdDisposable = this.commands.registerCommand( 'wordCount.showCount', this.showWordCount.bind(this) ); this.disposables.push(cmdDisposable); // Add status bar item const statusDisposable = this.statusBar.addItem({ id: 'word-count-status', text: this.getWordCountText(), tooltip: 'Word count for current document', command: 'wordCount.showCount', }); this.disposables.push(statusDisposable); this.notifications.showInfo('Word Count plugin activated'); } deactivate(): void { console.log('[WordCountPlugin] Deactivating...'); for (const disposable of this.disposables) { disposable.dispose(); } this.disposables = []; } private showWordCount(): void { const doc = this.documents.getActiveDocument(); if (!doc) { this.notifications.showWarning('No active document'); return; } const wordCount = this.countWords(doc.content); this.notifications.showInfo(`Word count: ${wordCount}`); } private getWordCountText(): string { const doc = this.documents.getActiveDocument(); if (!doc) return 'No document'; return `Words: ${this.countWords(doc.content)}`; } private countWords(text: string): number { return text.split(/\s+/).filter(word => word.length > 0).length; }} // ========================================// LOADING THE PLUGIN// ========================================const loader = new PluginLoader(editorContext);await loader.loadPlugin(new WordCountPlugin());Notice how the plugin only accesses what it declares through interfaces. It cannot access the CommandRegistry if it doesn't implement CommandRegistryInjector. This provides security through selective exposure — plugins only get the APIs they explicitly request.
Some dependencies need to know about the lifecycle of their consumers. Lifecycle-aware injection allows dependencies to perform actions when injected, and allows both the dependency and consumer to participate in lifecycle events.
Use cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
// ========================================// Lifecycle interfaces// ======================================== interface InitializingBean { /** * Called after all dependencies are injected */ afterPropertiesSet(): void | Promise<void>;} interface DisposableBean { /** * Called before the bean is destroyed */ destroy(): void | Promise<void>;} interface Lifecycle { /** * Start the component */ start(): void | Promise<void>; /** * Stop the component */ stop(): void | Promise<void>; /** * Is the component running? */ isRunning(): boolean;} interface RefreshAware { /** * Called when the container is refreshed */ onRefresh(): void | Promise<void>;} // ========================================// Container with lifecycle management// ======================================== class LifecycleAwareContainer { private beans: Map<string, object> = new Map(); private beanOrder: string[] = []; // Track creation order for destruction private started = false; // ... dependency registration and injection code ... /** * Register and initialize a bean */ async registerBean(name: string, bean: object): Promise<void> { console.log(`[Container] Registering bean: ${name}`); // Perform interface injection first this.injectDependencies(bean); // Call InitializingBean callback after injection if (this.isInitializingBean(bean)) { console.log(`[Container] Calling afterPropertiesSet on ${name}`); await bean.afterPropertiesSet(); } this.beans.set(name, bean); this.beanOrder.push(name); // If container is already started, start this bean too if (this.started && this.isLifecycle(bean)) { await bean.start(); } } /** * Start all Lifecycle beans */ async start(): Promise<void> { console.log('[Container] Starting all lifecycle beans...'); for (const name of this.beanOrder) { const bean = this.beans.get(name)!; if (this.isLifecycle(bean) && !bean.isRunning()) { console.log(`[Container] Starting: ${name}`); await bean.start(); } } this.started = true; console.log('[Container] All lifecycle beans started'); } /** * Stop all Lifecycle beans (reverse order) */ async stop(): Promise<void> { console.log('[Container] Stopping all lifecycle beans...'); // Stop in reverse order for (const name of [...this.beanOrder].reverse()) { const bean = this.beans.get(name)!; if (this.isLifecycle(bean) && bean.isRunning()) { console.log(`[Container] Stopping: ${name}`); await bean.stop(); } } this.started = false; console.log('[Container] All lifecycle beans stopped'); } /** * Destroy all beans (cleanup) */ async destroy(): Promise<void> { console.log('[Container] Destroying all beans...'); // Stop first if running if (this.started) { await this.stop(); } // Destroy in reverse order for (const name of [...this.beanOrder].reverse()) { const bean = this.beans.get(name)!; if (this.isDisposableBean(bean)) { console.log(`[Container] Destroying: ${name}`); await bean.destroy(); } } this.beans.clear(); this.beanOrder = []; console.log('[Container] All beans destroyed'); } /** * Refresh the container (reload configuration, etc.) */ async refresh(): Promise<void> { console.log('[Container] Refreshing...'); // Reload configuration await this.reloadConfiguration(); // Notify RefreshAware beans for (const [name, bean] of this.beans) { if (this.isRefreshAware(bean)) { console.log(`[Container] Notifying refresh: ${name}`); await bean.onRefresh(); } } console.log('[Container] Refresh complete'); } // Type guards private isInitializingBean(obj: unknown): obj is InitializingBean { return typeof (obj as InitializingBean)?.afterPropertiesSet === 'function'; } private isDisposableBean(obj: unknown): obj is DisposableBean { return typeof (obj as DisposableBean)?.destroy === 'function'; } private isLifecycle(obj: unknown): obj is Lifecycle { return typeof (obj as Lifecycle)?.start === 'function' && typeof (obj as Lifecycle)?.stop === 'function' && typeof (obj as Lifecycle)?.isRunning === 'function'; } private isRefreshAware(obj: unknown): obj is RefreshAware { return typeof (obj as RefreshAware)?.onRefresh === 'function'; } private injectDependencies(bean: object): void { // ... interface injection logic ... } private async reloadConfiguration(): Promise<void> { // ... reload config ... }} // ========================================// Example: Database Connection Pool with full lifecycle// ======================================== class DatabaseConnectionPool implements LoggerInjector, ConfigurationInjector, InitializingBean, DisposableBean, Lifecycle, RefreshAware { private logger!: Logger; private config!: Configuration; private pool: Connection[] = []; private running = false; private poolSize = 10; // Interface injection injectLogger(logger: Logger): void { this.logger = logger; } injectConfiguration(config: Configuration): void { this.config = config; } // Called after all dependencies are injected async afterPropertiesSet(): Promise<void> { this.logger.info('[Pool] Initializing after dependencies set'); this.poolSize = this.config.get<number>('database.poolSize') ?? 10; this.logger.info(`[Pool] Pool size configured to ${this.poolSize}`); } // Lifecycle: start async start(): Promise<void> { this.logger.info('[Pool] Starting - creating connections'); const connectionString = this.config.getRequired<string>('database.url'); for (let i = 0; i < this.poolSize; i++) { const conn = await this.createConnection(connectionString); this.pool.push(conn); } this.running = true; this.logger.info(`[Pool] Started with ${this.poolSize} connections`); } // Lifecycle: stop async stop(): Promise<void> { this.logger.info('[Pool] Stopping - closing connections'); for (const conn of this.pool) { await conn.close(); } this.pool = []; this.running = false; this.logger.info('[Pool] Stopped'); } isRunning(): boolean { return this.running; } // Called on configuration refresh async onRefresh(): Promise<void> { const newPoolSize = this.config.get<number>('database.poolSize') ?? 10; if (newPoolSize !== this.poolSize) { this.logger.info(`[Pool] Pool size changed: ${this.poolSize} -> ${newPoolSize}`); this.poolSize = newPoolSize; if (this.running) { // Adjust pool size at runtime await this.resizePool(newPoolSize); } } } // Called on container shutdown async destroy(): Promise<void> { this.logger.info('[Pool] Destroying'); if (this.running) { await this.stop(); } // Additional cleanup... } // Public API async getConnection(): Promise<Connection> { if (!this.running) { throw new Error('Pool not running'); } // ... connection acquisition logic ... } private async createConnection(connStr: string): Promise<Connection> { // ... create connection ... } private async resizePool(newSize: number): Promise<void> { // ... resize pool logic ... }} // ========================================// Application bootstrap// ======================================== const container = new LifecycleAwareContainer(); // Register beansawait container.registerBean('logger', new ConsoleLogger());await container.registerBean('config', new FileConfiguration('./config.json'));await container.registerBean('dbPool', new DatabaseConnectionPool()); // Start all lifecycle beansawait container.start(); // Run application... // On shutdownprocess.on('SIGTERM', async () => { await container.destroy(); process.exit(0);});The typical lifecycle order is: 1) Construct → 2) Inject → 3) afterPropertiesSet → 4) start → (running) → 5) stop → 6) destroy. This gives components multiple opportunities to initialize and clean up at the appropriate times.
Cross-cutting concerns are aspects that affect many components: logging, security, transactions, caching, metrics. Interface Injection enables systematic injection of these concerns across an application.
Advantages for cross-cutting concerns:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
// ========================================// Cross-cutting concern interfaces// ======================================== // Logging concerninterface LoggingAware { setLogger(logger: Logger): void;} // Metrics concerninterface MetricsAware { setMetrics(metrics: MetricsCollector): void;} // Transaction concerninterface TransactionAware { setTransactionManager(manager: TransactionManager): void;} // Security context concerninterface SecurityContextAware { setSecurityContext(context: SecurityContext): void;} // Distributed tracing concerninterface TracingAware { setTracer(tracer: DistributedTracer): void;} // Error handling concerninterface ErrorHandlerAware { setErrorHandler(handler: ErrorHandler): void;} // ========================================// Concern implementations// ======================================== interface Logger { debug(message: string, context?: object): void; info(message: string, context?: object): void; warn(message: string, context?: object): void; error(message: string, error?: Error, context?: object): void;} interface MetricsCollector { incrementCounter(name: string, labels?: Record<string, string>): void; recordHistogram(name: string, value: number, labels?: Record<string, string>): void; recordGauge(name: string, value: number, labels?: Record<string, string>): void;} interface TransactionManager { begin(): Transaction; runInTransaction<T>(fn: () => Promise<T>): Promise<T>;} interface SecurityContext { getCurrentUser(): User | undefined; hasPermission(permission: string): boolean; isAuthenticated(): boolean;} interface DistributedTracer { startSpan(name: string): Span; getCurrentSpan(): Span | undefined; injectContext(headers: Record<string, string>): void;} interface ErrorHandler { handle(error: Error, context?: object): void; captureException(error: Error): void;} // ========================================// Cross-cutting concern container// ======================================== class CrossCuttingConcernInjector { private logger: Logger; private metrics: MetricsCollector; private transactionManager: TransactionManager; private securityContext: SecurityContext; private tracer: DistributedTracer; private errorHandler: ErrorHandler; constructor(concerns: { logger: Logger; metrics: MetricsCollector; transactionManager: TransactionManager; securityContext: SecurityContext; tracer: DistributedTracer; errorHandler: ErrorHandler; }) { this.logger = concerns.logger; this.metrics = concerns.metrics; this.transactionManager = concerns.transactionManager; this.securityContext = concerns.securityContext; this.tracer = concerns.tracer; this.errorHandler = concerns.errorHandler; } /** * Inject all applicable cross-cutting concerns into a target */ injectConcerns(target: object): void { const injected: string[] = []; if (this.isLoggingAware(target)) { target.setLogger(this.logger); injected.push('Logger'); } if (this.isMetricsAware(target)) { target.setMetrics(this.metrics); injected.push('Metrics'); } if (this.isTransactionAware(target)) { target.setTransactionManager(this.transactionManager); injected.push('TransactionManager'); } if (this.isSecurityContextAware(target)) { target.setSecurityContext(this.securityContext); injected.push('SecurityContext'); } if (this.isTracingAware(target)) { target.setTracer(this.tracer); injected.push('Tracer'); } if (this.isErrorHandlerAware(target)) { target.setErrorHandler(this.errorHandler); injected.push('ErrorHandler'); } if (injected.length > 0) { this.logger.debug(`Injected concerns into ${target.constructor.name}`, { concerns: injected }); } } // Type guards private isLoggingAware(obj: unknown): obj is LoggingAware { return typeof (obj as LoggingAware)?.setLogger === 'function'; } private isMetricsAware(obj: unknown): obj is MetricsAware { return typeof (obj as MetricsAware)?.setMetrics === 'function'; } private isTransactionAware(obj: unknown): obj is TransactionAware { return typeof (obj as TransactionAware)?.setTransactionManager === 'function'; } private isSecurityContextAware(obj: unknown): obj is SecurityContextAware { return typeof (obj as SecurityContextAware)?.setSecurityContext === 'function'; } private isTracingAware(obj: unknown): obj is TracingAware { return typeof (obj as TracingAware)?.setTracer === 'function'; } private isErrorHandlerAware(obj: unknown): obj is ErrorHandlerAware { return typeof (obj as ErrorHandlerAware)?.setErrorHandler === 'function'; }} // ========================================// Service using multiple cross-cutting concerns// ======================================== class PaymentService implements LoggingAware, MetricsAware, TransactionAware, SecurityContextAware, TracingAware, ErrorHandlerAware { private logger!: Logger; private metrics!: MetricsCollector; private transactions!: TransactionManager; private security!: SecurityContext; private tracer!: DistributedTracer; private errorHandler!: ErrorHandler; // Cross-cutting concern injection methods setLogger(logger: Logger): void { this.logger = logger; } setMetrics(metrics: MetricsCollector): void { this.metrics = metrics; } setTransactionManager(manager: TransactionManager): void { this.transactions = manager; } setSecurityContext(context: SecurityContext): void { this.security = context; } setTracer(tracer: DistributedTracer): void { this.tracer = tracer; } setErrorHandler(handler: ErrorHandler): void { this.errorHandler = handler; } async processPayment(paymentRequest: PaymentRequest): Promise<PaymentResult> { // Start distributed trace const span = this.tracer.startSpan('processPayment'); // Check security if (!this.security.hasPermission('payment:process')) { this.logger.warn('Payment attempt without permission', { userId: this.security.getCurrentUser()?.id }); span.setError(new Error('Unauthorized')); throw new UnauthorizedError('permission:payment:process'); } // Record metrics this.metrics.incrementCounter('payment_attempts', { currency: paymentRequest.currency }); const startTime = Date.now(); try { // Run in transaction const result = await this.transactions.runInTransaction(async () => { this.logger.info('Processing payment', { amount: paymentRequest.amount, currency: paymentRequest.currency, }); // ... payment processing logic ... return { success: true, transactionId: 'txn-123' }; }); // Record success metrics const duration = Date.now() - startTime; this.metrics.recordHistogram('payment_duration_ms', duration); this.metrics.incrementCounter('payment_success'); this.logger.info('Payment processed successfully', { transactionId: result.transactionId }); span.finish(); return result; } catch (error) { // Handle error with error handler this.errorHandler.captureException(error as Error); this.metrics.incrementCounter('payment_failures'); this.logger.error('Payment processing failed', error as Error); span.setError(error); span.finish(); throw error; } }} // ========================================// Application bootstrap// ======================================== const concerns = new CrossCuttingConcernInjector({ logger: new StructuredLogger(), metrics: new PrometheusMetrics(), transactionManager: new PostgresTransactionManager(), securityContext: requestScopedSecurityContext, tracer: new JaegerTracer(), errorHandler: new SentryErrorHandler(),}); const paymentService = new PaymentService();concerns.injectConcerns(paymentService); // PaymentService now has all cross-cutting concerns availableInterface Injection for cross-cutting concerns is a form of Aspect-Oriented Programming (AOP). The aspects (logging, metrics, etc.) are woven into the components through interface implementation rather than metaclass manipulation or code generation.
We've explored the scenarios where Interface Injection provides unique value. While not the default choice for everyday dependency injection, it excels in framework design, plugin architectures, lifecycle management, and cross-cutting concern distribution.
| Use Case | Why Interface Injection | Key Benefit |
|---|---|---|
| Framework Extension Points | Framework doesn't know user types | Discovery-based injection |
| Plugin Architectures | Plugins developed independently | Security through selective exposure |
| Lifecycle-Aware Injection | Dependencies need lifecycle callbacks | Ordered initialization and cleanup |
| Cross-Cutting Concerns | Many components need same concerns | Uniform, opt-in distribution |
You now understand when Interface Injection provides unique advantages. Next, we'll compare Interface Injection with Constructor and Setter Injection in detail, helping you choose the right approach for any scenario.