Loading learning content...
Container registration defines what can be created. Resolution is the process of actually creating objects—traversing the dependency graph, instantiating classes in the correct order, injecting dependencies, and returning fully-wired instances ready for use.
Resolution is where the container's value becomes most apparent. What would require dozens of manual instantiation calls happens automatically. You ask for a UserController, and the container creates the controller, its UserService, the service's UserRepository, the repository's DbContext, and so on—all with the correct lifecycles, all with proper disposal tracking.
Understanding how resolution works is essential for debugging failures, optimizing performance, and designing systems that leverage container capabilities effectively.
By the end of this page, you will understand how containers resolve dependencies: graph traversal algorithms, constructor selection strategies, handling ambiguity, lazy vs eager resolution, scope management during resolution, resolution interception, and performance optimization techniques.
When you call container.resolve<IUserService>(), the container executes a sophisticated algorithm that we can break down into discrete steps:
Step 1: Registration Lookup
The container checks its registry: "Do I have a registration for IUserService?" If not, resolution fails immediately with a clear error message.
Step 2: Lifecycle Check If the registration is Singleton or Scoped, the container checks: "Have I already created an instance in the current scope?" If yes, it returns the cached instance—no further work needed.
Step 3: Constructor Analysis The container examines the implementation type's constructor(s) to determine what dependencies are needed. Most containers support a 'greediest constructor' strategy—using the constructor with the most parameters that can all be resolved.
Step 4: Recursive Dependency Resolution For each constructor parameter, the algorithm recurses: resolve each dependency. This creates a depth-first traversal of the dependency graph.
Step 5: Instantiation With all dependencies resolved, the container calls the constructor with the resolved instances, creating the final object.
Step 6: Post-Instantiation Some containers support post-instantiation hooks: initialization methods, property injection, or interception wrapping.
Step 7: Lifecycle Tracking The created instance is registered with the appropriate scope for lifecycle management and future disposal.
The dependencies of an application form a directed acyclic graph (DAG)—directed because dependencies flow one way (A depends on B, not vice versa), and acyclic because circular dependencies are disallowed. Resolution traverses this graph using depth-first search.
Visual Example:
Consider this dependency structure:
UserController
├── IUserService (UserService)
│ ├── IUserRepository (PostgresUserRepository)
│ │ ├── IDbContext (EfDbContext) [Scoped]
│ │ └── ILogger (ConsoleLogger) [Singleton]
│ └── IUserCache (RedisUserCache)
│ ├── IRedisClient (RedisClient) [Singleton]
│ └── ILogger (ConsoleLogger) [Singleton] ← Same instance!
└── ILogger (ConsoleLogger) [Singleton] ← Same instance!
Resolution Order (Depth-First):
UserControllerIUserService → Request UserServiceIUserRepository → Request PostgresUserRepositoryIDbContext → Create EfDbContext (Scoped - new instance)ILogger → Create ConsoleLogger (Singleton - cache for reuse)PostgresUserRepository with DbContext + LoggerIUserCache → Request RedisUserCacheIRedisClient → Create RedisClient (Singleton - cache for reuse)ILogger → Return cached ConsoleLogger (already created in step 5)RedisUserCache with RedisClient + LoggerUserService with Repository + CacheILogger → Return cached ConsoleLoggerUserController with UserService + Logger123456789101112131415161718192021222324252627282930313233343536
// VISUALIZATION OF RESOLUTION TRACE // Enable resolution tracing for debuggingcontainer.enableResolutionTracing(); const controller = container.resolve<UserController>(); // Console output showing resolution trace:/*RESOLVE UserController├─ RESOLVE IUserService (UserService)│ ├─ RESOLVE IUserRepository (PostgresUserRepository)│ │ ├─ RESOLVE IDbContext (EfDbContext)│ │ │ └─ CREATE EfDbContext [Scoped] {id: "ctx-001"}│ │ ├─ RESOLVE ILogger (ConsoleLogger)│ │ │ └─ CREATE ConsoleLogger [Singleton] {id: "log-001"}│ │ └─ CREATE PostgresUserRepository [Scoped] {id: "repo-001"}│ ├─ RESOLVE IUserCache (RedisUserCache)│ │ ├─ RESOLVE IRedisClient (RedisClient)│ │ │ └─ CREATE RedisClient [Singleton] {id: "redis-001"}│ │ ├─ RESOLVE ILogger (ConsoleLogger)│ │ │ └─ CACHED ConsoleLogger [Singleton] {id: "log-001"} <-- Reused!│ │ └─ CREATE RedisUserCache [Singleton] {id: "cache-001"}│ └─ CREATE UserService [Scoped] {id: "svc-001"}├─ RESOLVE ILogger (ConsoleLogger)│ └─ CACHED ConsoleLogger [Singleton] {id: "log-001"} <-- Reused!└─ CREATE UserController [Transient] {id: "ctrl-001"} Resolution complete: 1 Transient, 3 Scoped, 3 Singleton (1 reused)*/ // The trace shows:// - Depth-first resolution order// - Which instances are created vs cached// - Instance IDs for debugging// - Lifecycle of each resolved typeWhen resolution fails deep in a dependency chain, understanding the graph helps you trace backward to the root cause. If PostgresUserRepository fails to create because IDbContext is missing, the trace shows exactly which types led to that resolution request.
When a class has multiple constructors, the container must decide which one to use. Different containers employ different strategies, and understanding your container's strategy prevents surprising behavior.
| Strategy | Description | When It Fails | Common In |
|---|---|---|---|
| Greediest Constructor | Use constructor with most parameters that can all be resolved | When no constructor's parameters are all resolvable | Most modern containers |
| Single Constructor | Require exactly one public constructor | Class has multiple public constructors | Some strict containers |
| Attributed Constructor | Use constructor marked with specific attribute | No constructor is marked, or multiple are marked | Containers with DI attributes |
| Explicit Selection | Registration specifies which constructor | Registration doesn't specify and class has multiple | Flexible containers |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
// CONSTRUCTOR SELECTION SCENARIOS // ==============================================// GREEDIEST CONSTRUCTOR STRATEGY// ============================================== class UserService { // Constructor A: 2 parameters constructor(repository: IUserRepository, logger: ILogger) { // Used if both can be resolved and cache cannot } // Constructor B: 3 parameters (greediest) constructor( repository: IUserRepository, logger: ILogger, cache: IUserCache ) { // Used if ALL THREE can be resolved (preferred - most parameters) } // Constructor C: 1 parameter constructor(repository: IUserRepository) { // Used only if logger cannot be resolved }} // If IUserRepository, ILogger, and IUserCache are all registered:// → Constructor B is used (3 params, all resolvable) // If only IUserRepository and ILogger are registered:// → Constructor A is used (2 params, all resolvable) // ==============================================// ATTRIBUTED CONSTRUCTOR SELECTION// ============================================== class OrderService { // This constructor is ignored constructor(repository: IOrderRepository) { // Not used unless marked } // This constructor is explicitly selected @Inject() // or @InjectionConstructor, depending on framework constructor( repository: IOrderRepository, logger: ILogger, config: IConfiguration ) { // Always used because of @Inject attribute }} // ==============================================// EXPLICIT REGISTRATION-TIME SELECTION// ============================================== container.register<IPaymentService, PaymentService>({ lifecycle: Lifecycle.Singleton, // Explicitly specify which constructor to use useConstructor: [IPaymentGateway, ILogger], // Constructor taking these types}); // Or with factory for complete control:container.register<IPaymentService>( Lifecycle.Singleton, (resolver) => { // You control exactly how it's constructed const gateway = resolver.resolve<IPaymentGateway>(); const logger = resolver.resolve<ILogger>(); const config = resolver.resolve<IConfiguration>(); // Maybe some don't come from the container const secretKey = process.env.PAYMENT_SECRET_KEY; return new PaymentService(gateway, logger, config, secretKey); }); // ==============================================// COMMON PITFALL: AMBIGUOUS CONSTRUCTORS// ============================================== // ❌ PROBLEMATIC: Multiple constructors, all resolvableclass AmbiguousService { constructor(logger: ILogger) { /* ... */ } constructor(logger: ILogger, cache: ICache) { /* ... */ } constructor(logger: ILogger, cache: ICache, metrics: IMetrics) { /* ... */ }} // If all are resolvable, greediest wins.// But what if business logic requires the 2-param version? // ✅ SOLUTION: Use factory registration for explicit controlcontainer.register<IAmbiguousService>( Lifecycle.Scoped, (resolver) => new AmbiguousService( resolver.resolve<ILogger>(), resolver.resolve<ICache>() // Intentionally NOT resolving IMetrics ));Many experts recommend classes have a single public constructor. This eliminates ambiguity, makes dependencies explicit, and simplifies container configuration. Reserve multiple constructors for backward compatibility or testing scenarios.
Resolution can fail for various reasons. Understanding failure modes helps you diagnose issues quickly and design robust systems.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
// RESOLUTION FAILURE SCENARIOS AND SOLUTIONS // ==============================================// FAILURE: MISSING REGISTRATION// ============================================== try { // IPaymentGateway was never registered const payment = container.resolve<IPaymentGateway>();} catch (error) { // ResolutionError: No registration for type 'IPaymentGateway'. // Did you forget to call container.register<IPaymentGateway, ...>()?} // Solution: Add the registrationcontainer.register<IPaymentGateway, StripeGateway>(); // ==============================================// FAILURE: MISSING DEPENDENCY (DEEP IN CHAIN)// ============================================== // IOrderService depends on IPaymentGateway which isn't registeredtry { const orderService = container.resolve<IOrderService>();} catch (error) { // ResolutionError: Unable to resolve 'IOrderService'. // → Unable to resolve 'OrderService'. // → Missing registration for parameter 'IPaymentGateway'. // // Resolution chain: IOrderService → OrderService → IPaymentGateway (missing)} // The error shows the full chain - trace backward to find the missing piece // ==============================================// FAILURE: CIRCULAR DEPENDENCY// ============================================== // A depends on B, B depends on Aclass ServiceA { constructor(private serviceB: IServiceB) {}} class ServiceB { constructor(private serviceA: IServiceA) {} // Circular!} try { const serviceA = container.resolve<IServiceA>();} catch (error) { // ResolutionError: Circular dependency detected. // Chain: IServiceA → ServiceA → IServiceB → ServiceB → IServiceA (cycle!)} // Solution 1: Break cycle with Lazy<T>class ServiceBFixed { constructor(private serviceA: Lazy<IServiceA>) {} doSomething() { // ServiceA created only when accessed this.serviceA.value.method(); }} // Solution 2: Introduce mediator/event patternclass ServiceAFixed { constructor(private eventBus: IEventBus) {} // No direct B dependency} class ServiceBAlsoFixed { constructor(private eventBus: IEventBus) {} // No direct A dependency}// A and B communicate via events instead of direct calls // ==============================================// FAILURE: SCOPE MISMATCH// ============================================== // IDbContext is registered as Scopedcontainer.register<IDbContext, EfDbContext>(Lifecycle.Scoped); // Resolving scoped service at container level (no scope) failstry { const dbContext = container.resolve<IDbContext>(); // No scope!} catch (error) { // ResolutionError: Cannot resolve scoped service 'IDbContext' from root container. // Create a scope first using container.createScope().} // Solution: Create a scopeusing scope = container.createScope();const dbContext = scope.resolve<IDbContext>(); // Works! // ==============================================// OPTIONAL DEPENDENCIES// ============================================== // Sometimes a dependency should be optionalclass ResilientService { private cache?: ICache; constructor( private repository: IRepository, // Required @Optional() cache?: ICache // Optional - null if not registered ) { this.cache = cache; } async getData(id: string): Promise<Data> { // Use cache if available, fallback to repository if (this.cache) { const cached = await this.cache.get(id); if (cached) return cached; } return this.repository.findById(id); }} // Registration: Don't register ICache in some environments// Containerifneeded: container.register<ICache, RedisCache>(); // ResilientService still resolves - cache parameter is null // ==============================================// TRY-RESOLVE PATTERNS// ============================================== // Check if resolvable without throwingif (container.canResolve<IOptionalFeature>()) { const feature = container.resolve<IOptionalFeature>(); feature.activate();} // Or use Try patternconst result = container.tryResolve<IOptionalFeature>();if (result.success) { result.value.activate();} // Useful for feature flags based on registration presenceNot all dependencies need immediate resolution. Lazy resolution defers object creation until first use, which is valuable for:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
// LAZY AND DEFERRED RESOLUTION PATTERNS // ==============================================// LAZY<T> FOR DEFERRED CREATION// ============================================== // Lazy<T> wraps a factory function - value created on first .value accessclass ReportService { constructor( private repository: IReportRepository, // Created immediately private pdfGenerator: Lazy<IPdfGenerator> // Created only when needed ) {} async getReport(id: string): Promise<Report> { // This method doesn't need PDF generation return this.repository.findById(id); // pdfGenerator is NOT instantiated } async getReportAsPdf(id: string): Promise<Buffer> { const report = await this.repository.findById(id); // NOW pdfGenerator is instantiated (first access) return this.pdfGenerator.value.generate(report); // Subsequent calls reuse the same instance }} // Registration with Lazycontainer.register<Lazy<IPdfGenerator>>( Lifecycle.Transient, (resolver) => new Lazy(() => resolver.resolve<IPdfGenerator>())); // Or many containers support Lazy<T> automatically:container.enableAutoLazy(); // Resolve Lazy<T> automatically // ==============================================// FUNC<T> FOR ON-DEMAND FACTORY// ============================================== // Unlike Lazy<T>, Func<T> creates a NEW instance each callclass WorkerService { constructor( private jobFactory: () => IJobProcessor // Factory function ) {} async processJobs(jobs: Job[]) { await Promise.all( jobs.map(async job => { // Each job gets its own processor instance const processor = this.jobFactory(); try { await processor.process(job); } finally { processor.dispose(); } }) ); }} // Registrationcontainer.register<() => IJobProcessor>( Lifecycle.Singleton, // The factory is singleton (resolver) => () => resolver.resolve<IJobProcessor>() // But creates transient); // ==============================================// FUNC<TArg, TResult> FOR PARAMETERIZED FACTORY// ============================================== // Factory that takes parametersclass ScopedClientFactory { constructor( private createClient: (tenantId: string) => ITenantDbClient ) {} getClientForTenant(tenantId: string): ITenantDbClient { return this.createClient(tenantId); }} // Registration with parametercontainer.register<(tenantId: string) => ITenantDbClient>( Lifecycle.Singleton, (resolver) => { const baseConfig = resolver.resolve<IDbConfiguration>(); const logger = resolver.resolve<ILogger>(); return (tenantId: string) => { // Factory uses parameter plus resolved dependencies return new TenantDbClient( baseConfig.getConnectionString(tenantId), logger ); }; }); // ==============================================// COMPARING RESOLUTION PATTERNS// ============================================== class ComparisonDemo { constructor( // Immediate: Created now, always available private immediate: IImmediate, // Lazy: Created on first .value access, same instance thereafter private lazy: Lazy<ILazy>, // Factory: New instance every call private factory: () => IFactory, // Parameterized: New instance with custom configuration private paramFactory: (config: Config) => IConfigured ) {} demonstrate() { // immediate is ready now this.immediate.work(); // lazy creates once, reuses thereafter const lazy1 = this.lazy.value; // Created here const lazy2 = this.lazy.value; // Same instance as lazy1 console.log(lazy1 === lazy2); // true // factory creates new each time const factory1 = this.factory(); // New instance const factory2 = this.factory(); // Different new instance console.log(factory1 === factory2); // false // paramFactory creates with custom config each time const configured = this.paramFactory({ region: "us-east-1" }); }}| Pattern | When Created | Instance Reuse | Best For |
|---|---|---|---|
Direct T | At parent resolution | Per lifecycle | Always-needed dependencies |
Lazy<T> | First .value access | Yes, same instance | Expensive/conditional deps, cycle breaking |
Func<T> | Each call | No, new each time | Worker pools, temporary resources |
Func<TArg, T> | Each call with args | No, configured per call | Multi-tenant, dynamic configuration |
Container scopes partition the instance lifetime space. The most common example is HTTP request scope: each request gets its own scope, with scoped services (like DbContext) isolated between requests.
Understanding Scope Hierarchy:
Containers typically support nested scopes:
During resolution, the container walks up the scope chain to find instances:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
// SCOPE-AWARE RESOLUTION // ==============================================// BASIC SCOPE USAGE// ============================================== // Root container holds singletonsconst container = new Container();container.register<ILogger, ConsoleLogger>(Lifecycle.Singleton);container.register<IDbContext, EfDbContext>(Lifecycle.Scoped);container.register<IValidator, InputValidator>(Lifecycle.Transient); // Singleton: Same instance everywhereconst logger1 = container.resolve<ILogger>();const logger2 = container.resolve<ILogger>();console.log(logger1 === logger2); // true // Scoped: Requires a scope// container.resolve<IDbContext>(); // ERROR: No scope! { // Create scope for request 1 using scope1 = container.createScope(); // Scoped services are cached within scope const db1a = scope1.resolve<IDbContext>(); const db1b = scope1.resolve<IDbContext>(); console.log(db1a === db1b); // true - same scope // Transient: New instance every time const validator1 = scope1.resolve<IValidator>(); const validator2 = scope1.resolve<IValidator>(); console.log(validator1 === validator2); // false - transient // Logger is still the singleton const logger3 = scope1.resolve<ILogger>(); console.log(logger1 === logger3); // true - same singleton}// scope1 disposed, db1a and db1b are disposed { // Create scope for request 2 using scope2 = container.createScope(); const db2 = scope2.resolve<IDbContext>(); console.log(db1a === db2); // false - different scope // db2 is a FRESH instance for request 2} // ==============================================// NESTED SCOPES// ============================================== using requestScope = container.createScope(); // Resolve user for this requestconst currentUser = requestScope.resolve<ICurrentUser>(); // Spawn background work within requestusing backgroundScope = requestScope.createChildScope(); // Background scope inherits from request scopeconst userInBackground = backgroundScope.resolve<ICurrentUser>();console.log(currentUser === userInBackground); // true IF scoped to request // But background scope can have its own scoped servicesconst backgroundDb = backgroundScope.resolve<IDbContext>();// backgroundDb is specific to background scope, disposed independently // ==============================================// SCOPE IN WEB FRAMEWORKS// ============================================== // Express-style middleware integrationapp.use((req, res, next) => { // Create scope for this request req.scope = container.createScope(); // Capture current user in scope req.scope.registerInstance<ICurrentUser>( extractUserFromToken(req.headers.authorization) ); res.on("finish", () => { // Dispose scope when response completes req.scope.dispose(); }); next();}); // Controller handlerapp.get("/orders", async (req, res) => { // Resolve from request scope - gets request-specific DbContext const orderService = req.scope.resolve<IOrderService>(); // CurrentUser was registered in middleware const currentUser = req.scope.resolve<ICurrentUser>(); const orders = await orderService.getOrdersForUser(currentUser.id); res.json(orders);}); // ==============================================// SCOPE DISPOSAL AND CLEANUP// ============================================== class DisposableDbContext implements IDbContext, IDisposable { private connection: Connection; constructor(connectionString: string) { this.connection = createConnection(connectionString); } dispose() { // Called automatically when scope is disposed this.connection.close(); console.log("DbContext disposed, connection closed"); }} // Container tracks disposablescontainer.register<IDbContext, DisposableDbContext>(Lifecycle.Scoped); { using scope = container.createScope(); const db = scope.resolve<IDbContext>(); // Use db...} // <- scope.dispose() called, which calls db.dispose()When a scope is disposed, objects are disposed in reverse creation order. Object created last is disposed first. This ensures dependencies are still valid during disposal. If you depend on disposal order, document it clearly.
Advanced containers support resolution interception—the ability to hook into the resolution process to modify, wrap, or observe resolved instances. This enables powerful patterns like:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
// RESOLUTION INTERCEPTION PATTERNS // ==============================================// INTERCEPTOR INTERFACE// ============================================== interface IResolutionInterceptor { // Called after resolution, can modify or wrap the instance intercept<T>( context: ResolutionContext, resolved: T ): T;} interface ResolutionContext { serviceType: Type; implementationType: Type; lifecycle: Lifecycle; scope: IContainerScope;} // ==============================================// LOGGING INTERCEPTOR// ============================================== class LoggingInterceptor implements IResolutionInterceptor { constructor(private logger: ILogger) {} intercept<T>(context: ResolutionContext, resolved: T): T { this.logger.debug( `Resolved ${context.serviceType.name} → ${context.implementationType.name} [${context.lifecycle}]` ); return resolved; // Return unchanged }} // ==============================================// WRAPPING INTERCEPTOR (AOP)// ============================================== class MetricsInterceptor implements IResolutionInterceptor { constructor(private metrics: IMetrics) {} intercept<T>(context: ResolutionContext, resolved: T): T { // Only intercept IService implementations if (!context.serviceType.name.endsWith("Service")) { return resolved; } // Wrap with metrics proxy return this.createMetricsProxy(resolved, context.serviceType.name); } private createMetricsProxy<T>(target: T, serviceName: string): T { return new Proxy(target as object, { get: (obj, prop) => { const value = (obj as any)[prop]; if (typeof value !== "function") return value; // Wrap methods with timing return async (...args: any[]) => { const timer = this.metrics.startTimer(`${serviceName}.${String(prop)}`); try { return await value.apply(obj, args); } finally { timer.stop(); } }; }, }) as T; }} // ==============================================// VALIDATION INTERCEPTOR// ============================================== class ValidationInterceptor implements IResolutionInterceptor { intercept<T>(context: ResolutionContext, resolved: T): T { // Validate configuration objects if (context.serviceType.name.endsWith("Configuration")) { const config = resolved as any; if (config.validate && typeof config.validate === "function") { const errors = config.validate(); if (errors.length > 0) { throw new ConfigurationValidationError( `Invalid ${context.serviceType.name}: ${errors.join(", ")}` ); } } } return resolved; }} // ==============================================// REGISTERING INTERCEPTORS// ============================================== container.addInterceptor(new LoggingInterceptor(logger));container.addInterceptor(new MetricsInterceptor(metrics));container.addInterceptor(new ValidationInterceptor()); // Interceptors run in registration order// For each resolution: Logging → Metrics → Validation // ==============================================// DECORATOR PATTERN (EXPLICIT WRAPPING)// ============================================== // Alternative to interception: Explicit decorator registration // Base servicecontainer.register<IUserService, UserService>(); // Add decorators (inside-out wrapping)container.decorate<IUserService>((inner, resolver) => { const logger = resolver.resolve<ILogger>(); return new LoggingUserServiceDecorator(inner, logger);}); container.decorate<IUserService>((inner, resolver) => { const cache = resolver.resolve<ICache>(); return new CachingUserServiceDecorator(inner, cache);}); // Resolution order: // 1. Create UserService// 2. Wrap with LoggingUserServiceDecorator// 3. Wrap with CachingUserServiceDecorator// 4. Return CachingUserServiceDecorator // Call order: Caching → Logging → UserService → Logging → CachingContainer resolution isn't free. Reflection, constructor analysis, graph traversal, and object creation all cost CPU cycles. For most applications, this cost is negligible. But for high-throughput systems resolving thousands of objects per second, optimization matters.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
// RESOLUTION PERFORMANCE OPTIMIZATION // ==============================================// PRE-WARMING ON STARTUP// ============================================== async function bootstrap() { const container = configureContainer(); console.log("Pre-warming frequently-resolved services..."); const warmupStart = Date.now(); // Pre-resolve key singletons to pay construction cost once const warmupTypes = [ ILogger, IConfiguration, IDbConnectionPool, IRedisClient, IEventBus, ]; for (const type of warmupTypes) { container.resolve(type); } console.log(`Warmup complete in ${Date.now() - warmupStart}ms`); // Now accepting traffic - first requests won't pay warmup cost const app = container.resolve<Application>(); await app.start();} // ==============================================// COMPILED RESOLUTION (CONCEPTUAL)// ============================================== // Many modern containers "compile" resolution paths// Instead of reflection on every resolution: // Slow: Reflective resolutionfunction resolveReflective<T>(type: Type<T>): T { const registration = findRegistration(type); const constructor = registration.implementation; const params = getConstructorParameters(constructor); // Reflection! const args = params.map(p => resolveReflective(p.type)); // Recurse return new constructor(...args); // Dynamic construction} // Fast: Compiled resolution (generated at registration time)// Container generates code like:const compiledResolveUserService = (scope: IScope): IUserService => { // No reflection - compiled the resolution path return new UserService( compiledResolveUserRepository(scope), // Direct call scope.getSingleton(ILogger), // Hash lookup compiledResolveUserCache(scope) // Direct call );}; // ==============================================// BATCH PROCESSING OPTIMIZATION// ============================================== // ❌ SLOW: Create new scope per itemasync function processItemsSlow(items: Item[]) { for (const item of items) { using scope = container.createScope(); // New scope per item! const processor = scope.resolve<IProcessor>(); await processor.process(item); } // Dispose per item - high overhead} // ✅ FAST: Reuse scope for batchasync function processItemsFast(items: Item[]) { using scope = container.createScope(); // One scope for batch const processor = scope.resolve<IProcessor>(); // Resolve once for (const item of items) { await processor.process(item); }} // Single dispose at end // ==============================================// MEASURING RESOLUTION PERFORMANCE// ============================================== class ResolutionProfiler { private resolutionTimes = new Map<string, number[]>(); profile<T>(type: Type<T>): T { const start = performance.now(); const instance = this.container.resolve(type); const duration = performance.now() - start; const times = this.resolutionTimes.get(type.name) ?? []; times.push(duration); this.resolutionTimes.set(type.name, times); return instance; } report() { console.log("Resolution Performance Report:"); console.log("=============================="); const sorted = [...this.resolutionTimes.entries()] .map(([name, times]) => ({ name, count: times.length, avg: times.reduce((a, b) => a + b, 0) / times.length, max: Math.max(...times), })) .sort((a, b) => b.avg - a.avg); for (const { name, count, avg, max } of sorted) { console.log(`${name}: count=${count}, avg=${avg.toFixed(2)}ms, max=${max.toFixed(2)}ms`); } }} // Usage during development/testing:const profiler = new ResolutionProfiler(container);// ... run application scenarios ...profiler.report(); // Output:// Resolution Performance Report:// ==============================// UserService: count=1000, avg=0.05ms, max=15.2ms (first resolution cached)// OrderProcessor: count=500, avg=0.03ms, max=0.1ms// ...Container resolution is rarely the bottleneck in real applications. Database queries, network calls, and business logic dominate execution time. Always profile before optimizing—the cost isn't where you expect it to be.
Resolution is the runtime manifestation of your dependency configuration. Understanding its mechanics enables effective debugging, optimization, and architectural leveraging of container capabilities.
What's Next:
With resolution mechanics understood, the final page provides a survey of popular IoC container frameworks across different languages and ecosystems—their capabilities, philosophies, and when to choose each.
You now understand automatic dependency resolution: graph traversal algorithms, constructor selection, failure handling, lazy resolution, scope management, interception, and performance optimization. Next, we'll survey popular IoC container frameworks used across the industry.