Loading learning content...
In the previous page, we explored how dependencies define injector interfaces—contracts that specify how injection should occur. But a contract alone accomplishes nothing. We need concrete implementations that fulfill these contracts, providing actual dependency instances to clients.
This page examines the implementation side of Interface Injection: how dependency providers create, configure, and supply dependencies through the injector interface mechanism. We'll explore containers, factories, and the orchestration logic that makes Interface Injection work in production systems.
Understanding implementation patterns transforms Interface Injection from theoretical concept to practical tool.
By the end of this page, you will understand how to implement the provider side of Interface Injection—creating containers that detect interface implementations, manage dependency lifecycles, and perform injection automatically. You'll learn factory integration, lazy initialization, and scope management patterns.
In Interface Injection, the dependency provider (often a container or framework) has several critical responsibilities:
1. Dependency Creation — Instantiating concrete implementations of dependencies
2. Configuration — Applying configuration to dependency instances
3. Discovery — Detecting which clients implement which injector interfaces
4. Injection Orchestration — Calling the appropriate injection methods on clients
5. Lifecycle Management — Managing when dependencies are created, reused, and destroyed
The provider acts as the central orchestrator, bridging the gap between abstract injector interfaces and concrete runtime objects.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
// ========================================// Core dependency provider infrastructure// ======================================== /** * Registry of injectable dependencies and their factories */interface DependencyRegistry { register<T>(token: symbol, factory: () => T): void; resolve<T>(token: symbol): T; hasRegistration(token: symbol): boolean;} /** * Injector interface detector - identifies what injection a client needs */interface InjectorDetector { detectInjectors(target: object): InjectorInfo[];} interface InjectorInfo { interfaceType: symbol; injectionMethod: string; dependencyToken: symbol;} /** * Main dependency provider implementation */class DependencyProvider { private registry: Map<symbol, { factory: () => unknown; instance?: unknown }> = new Map(); private detectors: InjectorDetector[] = []; private injectionHistory: Map<object, symbol[]> = new Map(); // ======== DEPENDENCY CREATION ======== /** * Register a dependency with its factory */ registerSingleton<T>(token: symbol, factory: () => T): this { this.registry.set(token, { factory, instance: undefined }); console.log(`[Provider] Registered singleton: ${token.description}`); return this; } registerTransient<T>(token: symbol, factory: () => T): this { // Transient: always call factory, never cache this.registry.set(token, { factory, instance: Symbol('TRANSIENT_MARKER') // Marker indicating transient }); console.log(`[Provider] Registered transient: ${token.description}`); return this; } /** * Resolve a dependency instance */ resolve<T>(token: symbol): T { const entry = this.registry.get(token); if (!entry) { throw new Error(`No registration for: ${token.description}`); } // Check if transient if (typeof entry.instance === 'symbol' && entry.instance.description === 'TRANSIENT_MARKER') { return entry.factory() as T; } // Singleton: create once, cache if (entry.instance === undefined) { console.log(`[Provider] Creating singleton instance: ${token.description}`); entry.instance = entry.factory(); } return entry.instance as T; } // ======== DISCOVERY ======== /** * Register a detector for a specific injector interface pattern */ registerDetector(detector: InjectorDetector): this { this.detectors.push(detector); return this; } /** * Discover all injection requirements for a target */ private discoverInjectionNeeds(target: object): InjectorInfo[] { const allNeeds: InjectorInfo[] = []; for (const detector of this.detectors) { const needs = detector.detectInjectors(target); allNeeds.push(...needs); } return allNeeds; } // ======== INJECTION ORCHESTRATION ======== /** * Inject all required dependencies into a target object */ injectInto(target: object): void { console.log(`[Provider] Beginning injection into ${target.constructor.name}`); const needs = this.discoverInjectionNeeds(target); const injected: symbol[] = []; for (const need of needs) { if (!this.registry.has(need.dependencyToken)) { console.warn(`[Provider] No provider for ${need.dependencyToken.description}`); continue; } const dependency = this.resolve(need.dependencyToken); const method = (target as Record<string, Function>)[need.injectionMethod]; if (typeof method === 'function') { console.log(`[Provider] Injecting ${need.dependencyToken.description} via ${need.injectionMethod}`); method.call(target, dependency); injected.push(need.dependencyToken); } } this.injectionHistory.set(target, injected); console.log(`[Provider] Completed injection: ${injected.length} dependencies`); } /** * Create and inject in one step */ create<T extends object>(ctor: new () => T): T { const instance = new ctor(); this.injectInto(instance); return instance; } // ======== LIFECYCLE MANAGEMENT ======== /** * Check if an object has been injected */ hasBeenInjected(target: object): boolean { return this.injectionHistory.has(target); } /** * Get what was injected into a target */ getInjectionReceipt(target: object): symbol[] { return this.injectionHistory.get(target) || []; } /** * Clear all singleton instances (useful for testing) */ clearInstances(): void { for (const entry of this.registry.values()) { if (typeof entry.instance !== 'symbol') { entry.instance = undefined; } } this.injectionHistory.clear(); console.log('[Provider] Cleared all singleton instances'); }}Provider architecture components:
A crucial part of Interface Injection is detecting which injector interfaces a class implements. Unlike constructor injection where dependencies are explicit in the signature, Interface Injection requires runtime inspection.
Detection strategies:
injectLogger)inject*, set*)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
// ========================================// Strategy 1: Method Presence Detection// The simplest approach - check for specific method names// ======================================== // Dependency tokensconst LOGGER = Symbol('Logger');const CACHE = Symbol('Cache');const CONFIG = Symbol('Configuration');const METRICS = Symbol('Metrics'); // Detection helper for each injectable dependencyclass MethodPresenceDetector implements InjectorDetector { private mappings: { method: string; token: symbol }[] = [ { method: 'injectLogger', token: LOGGER }, { method: 'injectCache', token: CACHE }, { method: 'injectConfiguration', token: CONFIG }, { method: 'injectMetrics', token: METRICS }, ]; detectInjectors(target: object): InjectorInfo[] { const results: InjectorInfo[] = []; for (const { method, token } of this.mappings) { if (typeof (target as Record<string, unknown>)[method] === 'function') { results.push({ interfaceType: Symbol.for(method), injectionMethod: method, dependencyToken: token, }); } } return results; }} // ========================================// Strategy 2: Decorator-Based Detection// Uses TypeScript decorators to mark injectable methods// ======================================== // Symbol to store injection metadataconst INJECTION_METADATA = Symbol('injectionMetadata'); interface InjectionMetadata { method: string; token: symbol;} // Decorator to mark a method as an injection pointfunction Inject(token: symbol) { return function( target: object, propertyKey: string, descriptor: PropertyDescriptor ) { // Store metadata on the class prototype const existingMetadata: InjectionMetadata[] = Reflect.getMetadata(INJECTION_METADATA, target.constructor) || []; existingMetadata.push({ method: propertyKey, token: token, }); Reflect.defineMetadata( INJECTION_METADATA, existingMetadata, target.constructor ); };} // Detector that reads decorator metadataclass DecoratorBasedDetector implements InjectorDetector { detectInjectors(target: object): InjectorInfo[] { const metadata: InjectionMetadata[] = Reflect.getMetadata(INJECTION_METADATA, target.constructor) || []; return metadata.map(m => ({ interfaceType: Symbol.for(`Injectable_${m.method}`), injectionMethod: m.method, dependencyToken: m.token, })); }} // Usage with decoratorsclass DecoratedUserService { private logger!: Logger; private cache!: CacheService; @Inject(LOGGER) injectLogger(logger: Logger): void { this.logger = logger; } @Inject(CACHE) injectCache(cache: CacheService): void { this.cache = cache; } async findUser(id: string): Promise<User | null> { // Use injected dependencies this.logger.log(`Looking up user ${id}`); const cached = await this.cache.get<User>(`user:${id}`); if (cached) return cached; // Database lookup... }} // ========================================// Strategy 3: Convention-Based Detection// Matches methods by naming patterns// ======================================== class ConventionBasedDetector implements InjectorDetector { // Token resolution by convention private tokenMap: Map<string, symbol> = new Map([ ['Logger', LOGGER], ['Cache', CACHE], ['Configuration', CONFIG], ['Metrics', METRICS], ]); detectInjectors(target: object): InjectorInfo[] { const results: InjectorInfo[] = []; // Get all method names from prototype const prototype = Object.getPrototypeOf(target); const methodNames = Object.getOwnPropertyNames(prototype) .filter(name => typeof prototype[name] === 'function'); // Match methods starting with 'inject' for (const methodName of methodNames) { const match = methodName.match(/^inject(.+)$/); if (match) { const dependencyName = match[1]; const token = this.tokenMap.get(dependencyName); if (token) { results.push({ interfaceType: Symbol.for(`${dependencyName}Injector`), injectionMethod: methodName, dependencyToken: token, }); } } } return results; }} // ========================================// Strategy 4: Interface Symbol Detection// Classes explicitly declare what they implement via symbols// ======================================== // Interfaces are represented by symbolsconst LoggerInjectorSymbol = Symbol.for('LoggerInjector');const CacheInjectorSymbol = Symbol.for('CacheInjector'); // Interface to symbol mappinginterface InjectorInterface { readonly interfaceSymbol: symbol; readonly injectionMethod: string; readonly dependencyToken: symbol;} // Registry of known injector interfacesconst INJECTOR_REGISTRY: InjectorInterface[] = [ { interfaceSymbol: LoggerInjectorSymbol, injectionMethod: 'injectLogger', dependencyToken: LOGGER }, { interfaceSymbol: CacheInjectorSymbol, injectionMethod: 'injectCache', dependencyToken: CACHE },]; // Class declares implemented interfacesclass ExplicitUserService { // Static symbol array declares implemented interfaces static readonly implements: symbol[] = [ LoggerInjectorSymbol, CacheInjectorSymbol, ]; private logger!: Logger; private cache!: CacheService; injectLogger(logger: Logger): void { this.logger = logger; } injectCache(cache: CacheService): void { this.cache = cache; }} // Detector reads static interface declarationsclass SymbolBasedDetector implements InjectorDetector { detectInjectors(target: object): InjectorInfo[] { const constructor = target.constructor as { implements?: symbol[] }; const declaredInterfaces = constructor.implements || []; return INJECTOR_REGISTRY .filter(reg => declaredInterfaces.includes(reg.interfaceSymbol)) .map(reg => ({ interfaceType: reg.interfaceSymbol, injectionMethod: reg.injectionMethod, dependencyToken: reg.dependencyToken, })); }}Method presence is simplest but fragile (any method with the right name is detected). Decorator-based is explicit but requires decorator support. Convention-based is flexible but needs careful naming discipline. Symbol-based is most explicit but adds boilerplate. Many production frameworks combine multiple strategies for robustness.
Dependencies often require complex construction logic—configuration, external connections, conditional initialization. Factory integration allows Interface Injection systems to delegate construction to specialized factory objects.
Factory patterns in Interface Injection:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
// ========================================// Factory Interface// ======================================== interface DependencyFactory<T> { create(): T; destroy?(instance: T): void;} // ========================================// Simple Factory: Encapsulates construction logic// ======================================== class LoggerFactory implements DependencyFactory<Logger> { constructor( private readonly config: LoggingConfig, private readonly environment: string ) {} create(): Logger { console.log('[LoggerFactory] Creating Logger instance'); // Construction logic with configuration const logger = new CompositeLogger([ new ConsoleLogger(this.config.consoleLevel), ...(this.environment === 'production' ? [new CloudLogger(this.config.cloudEndpoint)] : []), ...(this.config.enableFileLogging ? [new FileLogger(this.config.logFilePath)] : []), ]); return logger; } destroy(instance: Logger): void { console.log('[LoggerFactory] Destroying Logger instance'); if ('close' in instance && typeof instance.close === 'function') { instance.close(); } }} // ========================================// Abstract Factory: Families of related dependencies// ======================================== interface DatabaseFactory { createConnection(): DatabaseConnection; createQueryBuilder(): QueryBuilder; createMigrationRunner(): MigrationRunner;} class PostgresFactory implements DatabaseFactory { constructor(private readonly connectionString: string) {} createConnection(): DatabaseConnection { return new PostgresConnection(this.connectionString); } createQueryBuilder(): QueryBuilder { return new PostgresQueryBuilder(); } createMigrationRunner(): MigrationRunner { return new PostgresMigrationRunner(this.createConnection()); }} class MySQLFactory implements DatabaseFactory { constructor(private readonly connectionString: string) {} createConnection(): DatabaseConnection { return new MySQLConnection(this.connectionString); } createQueryBuilder(): QueryBuilder { return new MySQLQueryBuilder(); } createMigrationRunner(): MigrationRunner { return new MySQLMigrationRunner(this.createConnection()); }} // ========================================// Parameterized Factory: Runtime parameters// ======================================== interface ParameterizedFactory<T, P> { create(params: P): T;} interface HttpClientParams { baseUrl: string; timeout?: number; retries?: number; headers?: Record<string, string>;} class HttpClientFactory implements ParameterizedFactory<HttpClient, HttpClientParams> { create(params: HttpClientParams): HttpClient { return new AxiosHttpClient({ baseURL: params.baseUrl, timeout: params.timeout ?? 30000, headers: { 'Content-Type': 'application/json', ...params.headers, }, }).withRetry(params.retries ?? 3); }} // ========================================// Conditional Factory: Environment-based selection// ======================================== class CacheFactory implements DependencyFactory<CacheService> { constructor( private readonly config: CacheConfig, private readonly environment: string ) {} create(): CacheService { console.log(`[CacheFactory] Creating cache for environment: ${this.environment}`); // Select implementation based on environment switch (this.environment) { case 'production': return new RedisCache({ cluster: this.config.redisCluster, keyPrefix: this.config.keyPrefix, defaultTTL: this.config.defaultTTL, }); case 'staging': // Staging uses Redis but different configuration return new RedisCache({ host: this.config.stagingRedisHost, keyPrefix: `staging:${this.config.keyPrefix}`, defaultTTL: this.config.defaultTTL / 2, // Shorter TTL in staging }); case 'development': case 'test': default: // Development uses in-memory cache return new InMemoryCache({ maxSize: 1000, defaultTTL: 60, // 1 minute }); } }} // ========================================// Integrating Factories with Dependency Provider// ======================================== class FactoryAwareProvider { private factories: Map<symbol, DependencyFactory<unknown>> = new Map(); private instances: Map<symbol, unknown> = new Map(); private parameterizedFactories: Map<symbol, ParameterizedFactory<unknown, unknown>> = new Map(); /** * Register a standard factory */ registerFactory<T>(token: symbol, factory: DependencyFactory<T>): this { this.factories.set(token, factory); return this; } /** * Register a parameterized factory */ registerParameterizedFactory<T, P>( token: symbol, factory: ParameterizedFactory<T, P> ): this { this.parameterizedFactories.set(token, factory as ParameterizedFactory<unknown, unknown>); return this; } /** * Resolve a dependency using its factory */ resolve<T>(token: symbol): T { // Check cache first if (this.instances.has(token)) { return this.instances.get(token) as T; } // Get factory and create const factory = this.factories.get(token); if (!factory) { throw new Error(`No factory registered for: ${token.description}`); } const instance = factory.create(); this.instances.set(token, instance); return instance as T; } /** * Resolve with parameters (always creates new instance) */ resolveWith<T, P>(token: symbol, params: P): T { const factory = this.parameterizedFactories.get(token); if (!factory) { throw new Error(`No parameterized factory for: ${token.description}`); } return factory.create(params) as T; } /** * Perform interface injection using factories */ injectInto(target: object): void { // LoggerInjector if (this.isLoggerInjector(target)) { target.injectLogger(this.resolve(LOGGER)); } // CacheInjector if (this.isCacheInjector(target)) { target.injectCache(this.resolve(CACHE)); } // ConfigInjector if (this.isConfigInjector(target)) { target.injectConfiguration(this.resolve(CONFIG)); } } /** * Cleanup: destroy all instances */ async destroyAll(): Promise<void> { for (const [token, instance] of this.instances) { const factory = this.factories.get(token); if (factory?.destroy) { console.log(`[Provider] Destroying: ${token.description}`); factory.destroy(instance); } } this.instances.clear(); } // Type guards... private isLoggerInjector(obj: unknown): obj is LoggerInjector { return typeof (obj as LoggerInjector)?.injectLogger === 'function'; } private isCacheInjector(obj: unknown): obj is CacheInjector { return typeof (obj as CacheInjector)?.injectCache === 'function'; } private isConfigInjector(obj: unknown): obj is ConfigInjector { return typeof (obj as ConfigInjector)?.injectConfiguration === 'function'; }} // Usageconst provider = new FactoryAwareProvider();const environment = process.env.NODE_ENV || 'development'; provider .registerFactory(LOGGER, new LoggerFactory(loggingConfig, environment)) .registerFactory(CACHE, new CacheFactory(cacheConfig, environment)) .registerParameterizedFactory(HTTP_CLIENT, new HttpClientFactory()); // Create and injectconst userService = new UserService();provider.injectInto(userService); // Get custom HTTP clientconst paymentClient = provider.resolveWith<HttpClient, HttpClientParams>( HTTP_CLIENT, { baseUrl: 'https://api.payment-processor.com', timeout: 60000 });Notice that factories can include destroy() methods for cleanup. This is crucial for dependencies with external resources (database connections, file handles, network connections). The provider should call these during application shutdown or scope cleanup.
Not all dependencies should be created at startup. Lazy initialization defers dependency creation until first use, improving startup time and avoiding unnecessary resource allocation.
Interface Injection pairs naturally with lazy initialization because the injection method provides a clear point where the dependency is needed.
Benefits of lazy initialization:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
// ========================================// Pattern 1: Lazy Proxy// Wraps a factory, creates real instance on first use// ======================================== function createLazyProxy<T extends object>(factory: () => T): T { let instance: T | null = null; return new Proxy({} as T, { get(target, prop) { if (!instance) { console.log('[LazyProxy] Creating instance on first access'); instance = factory(); } const value = (instance as Record<string | symbol, unknown>)[prop]; if (typeof value === 'function') { return value.bind(instance); } return value; }, set(target, prop, value) { if (!instance) { console.log('[LazyProxy] Creating instance on first write'); instance = factory(); } (instance as Record<string | symbol, unknown>)[prop] = value; return true; }, });} // Usageconst lazyLogger = createLazyProxy(() => { console.log('Creating expensive logger...'); return new CloudLogger({ endpoint: process.env.LOG_ENDPOINT, batchSize: 100, // Expensive setup... });}); // Logger not created yet // First call creates itlazyLogger.log('Hello'); // "Creating expensive logger..." then logs // Subsequent calls use cached instancelazyLogger.log('World'); // Just logs // ========================================// Pattern 2: Lazy Injection Method// Inject a getter instead of the instance// ======================================== interface LazyLoggerInjector { injectLazyLogger(loggerProvider: () => Logger): void;} class LazyUserService implements LazyLoggerInjector { private getLogger!: () => Logger; private _logger: Logger | null = null; injectLazyLogger(loggerProvider: () => Logger): void { console.log('[LazyUserService] Received logger provider (not yet created)'); this.getLogger = loggerProvider; } private get logger(): Logger { if (!this._logger) { console.log('[LazyUserService] First logger access - creating'); this._logger = this.getLogger(); } return this._logger; } createUser(data: UserData): User { // Logger created on first actual use this.logger.log(`Creating user: ${data.email}`); // ... }} // Provider injects the factory, not the instanceclass LazyAwareProvider { private loggerFactory: () => Logger; injectInto(target: object): void { if (this.isLazyLoggerInjector(target)) { // Inject the factory, not an instance target.injectLazyLogger(() => this.loggerFactory()); } } private isLazyLoggerInjector(obj: unknown): obj is LazyLoggerInjector { return typeof (obj as LazyLoggerInjector)?.injectLazyLogger === 'function'; }} // ========================================// Pattern 3: Deferred Initialization Container// Track which dependencies are lazy and resolve on demand// ======================================== type LazyResolution<T> = { type: 'lazy'; factory: () => T; instance?: T;}; type EagerResolution<T> = { type: 'eager'; instance: T;}; type Resolution<T> = LazyResolution<T> | EagerResolution<T>; class DeferredContainer { private resolutions: Map<symbol, Resolution<unknown>> = new Map(); /** * Register lazy - factory called on first resolve */ registerLazy<T>(token: symbol, factory: () => T): this { this.resolutions.set(token, { type: 'lazy', factory, }); console.log(`[Container] Registered lazy: ${token.description}`); return this; } /** * Register eager - instance created immediately */ registerEager<T>(token: symbol, factory: () => T): this { console.log(`[Container] Creating eager: ${token.description}`); this.resolutions.set(token, { type: 'eager', instance: factory(), }); return this; } /** * Resolve - creates lazy instances on first access */ resolve<T>(token: symbol): T { const resolution = this.resolutions.get(token); if (!resolution) { throw new Error(`Not registered: ${token.description}`); } if (resolution.type === 'eager') { return resolution.instance as T; } // Lazy resolution if (resolution.instance === undefined) { console.log(`[Container] Lazy-creating: ${token.description}`); resolution.instance = resolution.factory(); } return resolution.instance as T; } /** * Check if a lazy dependency has been instantiated */ isInstantiated(token: symbol): boolean { const resolution = this.resolutions.get(token); if (!resolution) return false; if (resolution.type === 'eager') return true; return resolution.instance !== undefined; } /** * Preload specific lazy dependencies */ preload(...tokens: symbol[]): void { for (const token of tokens) { this.resolve(token); } }} // Usageconst container = new DeferredContainer() // Critical services created immediately .registerEager(LOGGER, () => new ConsoleLogger()) .registerEager(CONFIG, () => Configuration.load()) // Expensive services created on demand .registerLazy(DATABASE, () => new DatabasePool(connectionConfig)) .registerLazy(CACHE, () => new RedisCache(cacheConfig)) .registerLazy(SEARCH_INDEX, () => new ElasticSearchClient(searchConfig)); // On startup, only LOGGER and CONFIG existconsole.log('Database created?', container.isInstantiated(DATABASE)); // false // First database access creates itconst db = container.resolve(DATABASE);console.log('Database created?', container.isInstantiated(DATABASE)); // trueLazy initialization delays errors to runtime. A misconfigured database won't fail at startup—it fails on first query. Consider "fast-fail" patterns where critical dependencies are preloaded even if configured as lazy, or add explicit validation that can run early without full initialization.
Interface Injection must manage dependency scopes—determining when dependencies are created, shared, and destroyed. Scope management becomes complex in multi-threaded or request-based architectures.
Common scopes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
// ========================================// Scope abstraction// ======================================== interface Scope { readonly id: string; get<T>(token: symbol): T | undefined; set<T>(token: symbol, value: T): void; clear(token: symbol): void; dispose(): void;} // ========================================// Request Scope: One scope per HTTP request// ======================================== class RequestScope implements Scope { readonly id: string; private instances: Map<symbol, unknown> = new Map(); private disposers: (() => void)[] = []; constructor() { this.id = crypto.randomUUID(); console.log(`[RequestScope] Created scope: ${this.id}`); } get<T>(token: symbol): T | undefined { return this.instances.get(token) as T | undefined; } set<T>(token: symbol, value: T): void { this.instances.set(token, value); } registerDisposer(disposer: () => void): void { this.disposers.push(disposer); } clear(token: symbol): void { this.instances.delete(token); } dispose(): void { console.log(`[RequestScope] Disposing scope: ${this.id}`); for (const disposer of this.disposers) { try { disposer(); } catch (e) { console.error(e); } } this.instances.clear(); this.disposers = []; }} // ========================================// AsyncLocal storage for request scope// (Similar to thread-local but for async contexts)// ======================================== import { AsyncLocalStorage } from 'async_hooks'; class ScopeManager { private static asyncStorage = new AsyncLocalStorage<RequestScope>(); /** * Get current request scope */ static getCurrentScope(): RequestScope | undefined { return this.asyncStorage.getStore(); } /** * Run a function within a new request scope */ static runInScope<T>(fn: () => T): T { const scope = new RequestScope(); try { return this.asyncStorage.run(scope, fn); } finally { // Scope disposed when request ends } } /** * Run async function in scope */ static async runInScopeAsync<T>(fn: () => Promise<T>): Promise<T> { const scope = new RequestScope(); try { return await this.asyncStorage.run(scope, fn); } finally { scope.dispose(); } }} // ========================================// Scoped Dependency Provider// ======================================== enum LifetimeScope { Singleton, Transient, Scoped,} interface ScopedRegistration<T> { factory: () => T; scope: LifetimeScope;} class ScopedDependencyProvider { private registrations: Map<symbol, ScopedRegistration<unknown>> = new Map(); private singletons: Map<symbol, unknown> = new Map(); register<T>( token: symbol, factory: () => T, scope: LifetimeScope = LifetimeScope.Transient ): this { this.registrations.set(token, { factory, scope }); return this; } resolve<T>(token: symbol): T { const registration = this.registrations.get(token); if (!registration) { throw new Error(`Not registered: ${token.description}`); } switch (registration.scope) { case LifetimeScope.Singleton: return this.resolveSingleton(token, registration.factory) as T; case LifetimeScope.Scoped: return this.resolveScoped(token, registration.factory) as T; case LifetimeScope.Transient: default: return registration.factory() as T; } } private resolveSingleton<T>(token: symbol, factory: () => T): T { if (!this.singletons.has(token)) { console.log(`[Provider] Creating singleton: ${token.description}`); this.singletons.set(token, factory()); } return this.singletons.get(token) as T; } private resolveScoped<T>(token: symbol, factory: () => T): T { const scope = ScopeManager.getCurrentScope(); if (!scope) { // No scope - fall back to transient console.warn(`[Provider] No scope for scoped dependency: ${token.description}`); return factory(); } let instance = scope.get<T>(token); if (!instance) { console.log(`[Provider] Creating scoped: ${token.description} in scope ${scope.id}`); instance = factory(); scope.set(token, instance); } return instance; } /** * Perform interface injection with scope awareness */ injectInto(target: object): void { // Each injector interface maps to a token if (this.requiresLogger(target)) { target.injectLogger(this.resolve(LOGGER)); } // UnitOfWork is typically scoped per request if (this.requiresUnitOfWork(target)) { target.injectUnitOfWork(this.resolve(UNIT_OF_WORK)); } // User context is scoped (one per request) if (this.requiresUserContext(target)) { target.injectUserContext(this.resolve(USER_CONTEXT)); } } // Type guards... private requiresLogger(obj: unknown): obj is LoggerInjector { return typeof (obj as LoggerInjector)?.injectLogger === 'function'; } private requiresUnitOfWork(obj: unknown): obj is UnitOfWorkInjector { return typeof (obj as UnitOfWorkInjector)?.injectUnitOfWork === 'function'; } private requiresUserContext(obj: unknown): obj is UserContextInjector { return typeof (obj as UserContextInjector)?.injectUserContext === 'function'; }} // ========================================// Usage in HTTP middleware// ======================================== const provider = new ScopedDependencyProvider() .register(LOGGER, () => new ConsoleLogger(), LifetimeScope.Singleton) .register(DATABASE, () => DatabasePool.instance, LifetimeScope.Singleton) .register(UNIT_OF_WORK, () => new UnitOfWork(provider.resolve(DATABASE)), LifetimeScope.Scoped) .register(USER_CONTEXT, () => new UserContext(), LifetimeScope.Scoped); // Express middlewareapp.use(async (req, res, next) => { await ScopeManager.runInScopeAsync(async () => { // Create the scoped service const userService = new UserService(); provider.injectInto(userService); // UserContext and UnitOfWork are scoped - same instances throughout request // They're automatically disposed when scope ends req.userService = userService; next(); });});Scoped dependencies are essential in web applications where request-specific state (user identity, transaction context, request correlation IDs) must be available throughout the request lifecycle without explicitly passing them through every function call.
We've explored how implementations provide dependencies through the Interface Injection mechanism. The provider side is where the abstract concepts become concrete—where factories create dependencies, detectors discover injection needs, and scope managers ensure correct lifecycle behavior.
You now understand how to implement the provider side of Interface Injection—from detection to factory integration to scope management. Next, we'll explore specific use cases where Interface Injection provides unique advantages over other injection types.