Loading content...
An IoC container is only as useful as its configuration. A poorly configured container leads to confusing errors, runtime failures, and architectural drift. A well-configured container becomes the architectural backbone of your application—documenting dependencies, enforcing boundaries, and enabling seamless testing and deployment.
Container configuration is where theory meets practice. It's where you translate architectural decisions into executable registrations. It's also where subtle mistakes can cause cascading failures that are notoriously difficult to debug.
This page explores container configuration comprehensively: registration strategies, modular organization, handling complex scenarios, environment-specific configuration, and the patterns that distinguish maintainable configurations from configuration nightmares.
By the end of this page, you will master container configuration strategies: explicit vs convention-based registration, organizing registrations into cohesive modules, handling complex scenarios like open generics and decorators, environment-specific configuration, validation and diagnostics, and configuration patterns used in production systems.
At its core, container registration answers one question: "When somebody asks for X, what should they get?" This mapping between requested types and provided implementations is the foundation of all container configuration.
The Registration Equation:
Service Type → Implementation Type + Lifecycle + Optional Configuration
Let's break down each component:
Service Type (Interface/Abstract):
The type that consuming code depends upon. This is typically an interface or abstract class—the abstraction from the Dependency Inversion Principle. When code declares constructor(private logger: ILogger), ILogger is the service type.
Implementation Type (Concrete):
The actual class that will be instantiated. When the container sees a request for ILogger, it might provide a ConsoleLogger, FileLogger, or CloudWatchLogger. The implementation type is the concretion.
Lifecycle: How long the created instance should live. Singleton (one forever), Scoped (one per scope), or Transient (one per resolution).
Optional Configuration: Some registrations require additional configuration—factory functions, constructor parameters, initialization callbacks, or conditional logic.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
// ANATOMY OF CONTAINER REGISTRATIONS // ==============================================// BASIC REGISTRATION: Service → Implementation// ============================================== // Most straightforward: interface maps to implementationcontainer.register<ILogger, ConsoleLogger>();// When anyone asks for ILogger, give them ConsoleLogger container.register<IUserRepository, PostgresUserRepository>();// IUserRepository requests → PostgresUserRepository instances // ==============================================// WITH LIFECYCLE: Service → Implementation + Lifecycle// ============================================== // Singleton: One instance forevercontainer.register<IConfiguration, AppConfiguration>(Lifecycle.Singleton);// First resolution creates instance; all subsequent resolutions reuse it // Scoped: One instance per scope (e.g., HTTP request)container.register<IDbContext, EfDbContext>(Lifecycle.Scoped);// Each request gets its own DbContext; disposed when request ends // Transient: New instance every timecontainer.register<IValidator, InputValidator>(Lifecycle.Transient);// Every resolution creates a fresh instance // ==============================================// WITH FACTORY: Custom instantiation logic// ============================================== // Factory function receives the resolver for accessing other dependenciescontainer.register<IPaymentGateway>( Lifecycle.Singleton, (resolver) => { const config = resolver.resolve<IConfiguration>(); const logger = resolver.resolve<ILogger>(); // Conditional implementation based on configuration if (config.get("PAYMENT_PROVIDER") === "stripe") { return new StripePaymentGateway( config.get("STRIPE_API_KEY"), logger ); } else { return new BraintreePaymentGateway( config.get("BRAINTREE_MERCHANT_ID"), logger ); } }); // ==============================================// WITH INSTANCE: Pre-created object// ============================================== // For objects created outside the containerconst externalLogger = createLoggerFromThirdPartyFramework();container.registerInstance<ILogger>(externalLogger); // ==============================================// SELF-REGISTRATION: Concrete type only// ============================================== // When you don't need an abstractioncontainer.register<ConcreteService>(Lifecycle.Transient);// Requested as ConcreteService, returned as ConcreteServiceContainer configuration strategies fall on a spectrum from fully explicit (every registration written manually) to fully conventional (registrations inferred from naming patterns and attributes). Most production systems use a hybrid approach, leveraging conventions for the majority of registrations while using explicit configuration for exceptions and complex cases.
| Strategy | Pros | Cons | Best For |
|---|---|---|---|
| Explicit Registration | Full control, clear and explicit, easy to understand | Verbose, requires updating for each new type | Complex services, conditional logic, external integrations |
| Convention-Based | Minimal boilerplate, scales automatically, enforces patterns | Magic behavior, harder to debug, requires team discipline | Standard services following patterns |
| Attribute-Based | Self-documenting, visible at class definition | Couples classes to DI framework, not always possible | Application-owned classes with clear lifecycles |
| Assembly Scanning | Zero-touch for new types, comprehensive | Can register unintended types, startup performance cost | Large applications with consistent patterns |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
// REGISTRATION STRATEGIES DEEP DIVE // ==============================================// STRATEGY 1: FULLY EXPLICIT// ==============================================// Every single type registered manually// Maximum control, maximum verbosity function configureExplicitRegistrations(container: Container) { // Infrastructure container.register<ILogger, ConsoleLogger>(Lifecycle.Singleton); container.register<IConfiguration, EnvConfiguration>(Lifecycle.Singleton); container.register<IMetrics, PrometheusMetrics>(Lifecycle.Singleton); // Data Access container.register<IDbConnectionFactory, PostgresConnectionFactory>(Lifecycle.Singleton); container.register<IUserRepository, PostgresUserRepository>(Lifecycle.Scoped); container.register<IOrderRepository, PostgresOrderRepository>(Lifecycle.Scoped); container.register<IProductRepository, PostgresProductRepository>(Lifecycle.Scoped); container.register<IInventoryRepository, PostgresInventoryRepository>(Lifecycle.Scoped); // Caching container.register<ICacheClient, RedisClient>(Lifecycle.Singleton); container.register<IUserCache, RedisUserCache>(Lifecycle.Singleton); container.register<IProductCache, RedisProductCache>(Lifecycle.Singleton); // Business Services container.register<IUserService, UserService>(Lifecycle.Scoped); container.register<IOrderService, OrderService>(Lifecycle.Scoped); container.register<IInventoryService, InventoryService>(Lifecycle.Scoped); container.register<ICheckoutService, CheckoutService>(Lifecycle.Scoped); // ... 50 more lines for a typical application} // ==============================================// STRATEGY 2: CONVENTION-BASED// ==============================================// Types registered based on naming patterns// Minimal code, requires team discipline function configureConventionRegistrations(container: Container) { // Convention: All IXxxRepository → XxxRepository, Scoped container.registerByConvention({ scan: "./src/repositories/**/*.ts", interfacePattern: /^I(w+)Repository$/, implementationPattern: (match) => new RegExp(`^${match[1]}Repository$`), lifecycle: Lifecycle.Scoped, }); // Convention: All IXxxService → XxxService, Scoped container.registerByConvention({ scan: "./src/services/**/*.ts", interfacePattern: /^I(w+)Service$/, implementationPattern: (match) => new RegExp(`^${match[1]}Service$`), lifecycle: Lifecycle.Scoped, }); // Convention: All IXxxHandler → XxxHandler, Transient container.registerByConvention({ scan: "./src/handlers/**/*.ts", interfacePattern: /^I(w+)Handler$/, implementationPattern: (match) => new RegExp(`^${match[1]}Handler$`), lifecycle: Lifecycle.Transient, }); // Explicit overrides for exceptional cases container.register<IPaymentService, StripePaymentService>(Lifecycle.Singleton);} // ==============================================// STRATEGY 3: ATTRIBUTE/DECORATOR-BASED// ==============================================// Registration information on the class itself @Injectable({ providedIn: 'root', // Singleton useClass: ConsoleLogger })export abstract class ILogger { abstract log(message: string): void;} @Injectable({ scope: 'request' }) // Scoped to HTTP requestexport class UserService implements IUserService { constructor( private readonly repository: IUserRepository, private readonly logger: ILogger ) {}} // Container scans for @Injectable and registers automaticallycontainer.scan("./src/**/*.ts", { decorator: Injectable }); // ==============================================// STRATEGY 4: ASSEMBLY/MODULE SCANNING // ==============================================// Automatic registration based on interface implementation container.scan("./src/**/*.ts", { // Register all classes implementing these marker interfaces registerImplementationsOf: [ IRepository, // Base interface for all repositories IService, // Base interface for all services IHandler, // Base interface for all handlers ], // Configuration per base interface lifecycleRules: { [IRepository.name]: Lifecycle.Scoped, [IService.name]: Lifecycle.Scoped, [IHandler.name]: Lifecycle.Transient, },});Most successful production systems use conventions for 80% of registrations (standard repositories, services, handlers) and explicit registration for the 20% that require special handling (third-party integrations, conditional implementations, complex factories). This balances productivity with control.
As applications grow, putting all registrations in a single file becomes unmaintainable. Modular configuration organizes registrations into cohesive units, typically aligned with architectural layers or bounded contexts.
Benefits of Modular Organization:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
// MODULAR CONFIGURATION ORGANIZATION // ==============================================// MODULE DEFINITION INTERFACE// ============================================== interface ContainerModule { name: string; register(container: Container): void | Promise<void>; dependsOn?: string[]; // Module dependencies} // ==============================================// LAYER-BASED MODULES// ============================================== // Infrastructure Moduleconst InfrastructureModule: ContainerModule = { name: "Infrastructure", register(container) { // Logging container.register<ILogger, StructuredLogger>(Lifecycle.Singleton); container.register<ILoggerFactory, SerilogFactory>(Lifecycle.Singleton); // Configuration container.register<IConfiguration, EnvConfiguration>(Lifecycle.Singleton); container.register<ISecretsManager, VaultSecretsManager>(Lifecycle.Singleton); // External Communication container.register<IHttpClient, AxiosHttpClient>(Lifecycle.Singleton); container.register<IEventBus, RabbitMqEventBus>(Lifecycle.Singleton); // Metrics & Monitoring container.register<IMetrics, PrometheusMetrics>(Lifecycle.Singleton); container.register<ITracer, JaegerTracer>(Lifecycle.Singleton); },}; // Data Access Moduleconst DataAccessModule: ContainerModule = { name: "DataAccess", dependsOn: ["Infrastructure"], register(container) { // Database Connections container.register<IDbConnectionFactory, PostgresConnectionFactory>(Lifecycle.Singleton); container.register<IDbContext, EfDbContext>(Lifecycle.Scoped); // Repositories (convention-based) container.registerByConvention({ scan: "./src/data-access/repositories/**/*.ts", interfacePattern: /^I(w+)Repository$/, lifecycle: Lifecycle.Scoped, }); // Unit of Work container.register<IUnitOfWork, DbUnitOfWork>(Lifecycle.Scoped); },}; // Business Logic Moduleconst BusinessModule: ContainerModule = { name: "Business", dependsOn: ["DataAccess"], register(container) { // Domain Services container.registerByConvention({ scan: "./src/business/services/**/*.ts", interfacePattern: /^I(w+)Service$/, lifecycle: Lifecycle.Scoped, }); // Command/Query Handlers container.registerByConvention({ scan: "./src/business/handlers/**/*.ts", interfacePattern: /^I(w+)Handler$/, lifecycle: Lifecycle.Transient, }); // Domain Event Handlers container.register<IDomainEventDispatcher, MediatorEventDispatcher>(Lifecycle.Scoped); },}; // Presentation Module const PresentationModule: ContainerModule = { name: "Presentation", dependsOn: ["Business"], register(container) { // Controllers (auto-registered by framework typically) // Middleware container.register<IAuthMiddleware, JwtAuthMiddleware>(Lifecycle.Scoped); container.register<ILoggingMiddleware, RequestLoggingMiddleware>(Lifecycle.Scoped); container.register<IErrorMiddleware, GlobalErrorMiddleware>(Lifecycle.Singleton); // View Services container.register<IViewRenderer, ReactViewRenderer>(Lifecycle.Scoped); },}; // ==============================================// MODULE LOADER// ============================================== class ModuleLoader { private loadedModules = new Set<string>(); constructor(private container: Container) {} async loadModule(module: ContainerModule): Promise<void> { // Skip already loaded if (this.loadedModules.has(module.name)) return; // Load dependencies first if (module.dependsOn) { for (const depName of module.dependsOn) { const depModule = this.findModule(depName); if (depModule) { await this.loadModule(depModule); } else { throw new Error( `Module "${module.name}" depends on "${depName}" which was not found` ); } } } // Load this module console.log(`Loading module: ${module.name}`); await module.register(this.container); this.loadedModules.add(module.name); } async loadAll(modules: ContainerModule[]): Promise<void> { for (const module of modules) { await this.loadModule(module); } }} // ==============================================// COMPOSITION ROOT USING MODULES// ============================================== async function bootstrap() { const container = new Container(); const loader = new ModuleLoader(container); await loader.loadAll([ InfrastructureModule, DataAccessModule, BusinessModule, PresentationModule, ]); const app = container.resolve<Application>(); await app.start();}In domain-driven design, modules often align with bounded contexts rather than technical layers. An 'OrdersModule' might contain repositories, services, and handlers all related to order management. This aligns dependencies with domain boundaries rather than technical boundaries.
Beyond basic type registration, production containers support sophisticated patterns for complex dependency scenarios. Understanding these patterns is essential for handling real-world architectural requirements.
IRepository<> once, resolve IRepository<User>, IRepository<Order>, etc. automatically123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
// ADVANCED REGISTRATION PATTERNS // ==============================================// PATTERN 1: OPEN GENERICS// ==============================================// Register a generic type once, resolve any closed version // Base repository interfaceinterface IRepository<T> { findById(id: string): Promise<T | null>; save(entity: T): Promise<void>; delete(id: string): Promise<void>;} // Generic implementationclass GenericRepository<T> implements IRepository<T> { constructor( private dbContext: IDbContext, private entityType: string ) {} async findById(id: string): Promise<T | null> { return this.dbContext.find(this.entityType, id); } // ... other methods} // Open generic registrationcontainer.registerOpenGeneric( IRepository, // Open generic interface GenericRepository, // Open generic implementation Lifecycle.Scoped); // Now all of these work automatically:const userRepo = container.resolve<IRepository<User>>();const orderRepo = container.resolve<IRepository<Order>>();const productRepo = container.resolve<IRepository<Product>>(); // ==============================================// PATTERN 2: DECORATORS (WRAPPER PATTERN)// ==============================================// Add cross-cutting concerns to resolved objects // Base serviceclass UserService implements IUserService { async getUser(id: string): Promise<User> { // Core business logic }} // Logging decoratorclass LoggingUserServiceDecorator implements IUserService { constructor( private inner: IUserService, private logger: ILogger ) {} async getUser(id: string): Promise<User> { this.logger.log(`Getting user: ${id}`); const startTime = Date.now(); try { const result = await this.inner.getUser(id); this.logger.log(`Got user in ${Date.now() - startTime}ms`); return result; } catch (error) { this.logger.log(`Failed to get user: ${error}`); throw error; } }} // Caching decoratorclass CachingUserServiceDecorator implements IUserService { constructor( private inner: IUserService, private cache: ICache ) {} async getUser(id: string): Promise<User> { const cached = await this.cache.get(`user:${id}`); if (cached) return cached; const user = await this.inner.getUser(id); await this.cache.set(`user:${id}`, user, { ttl: 300 }); return user; }} // Decorator registration (order matters - outermost first)container.register<IUserService, UserService>();container.decorate<IUserService, CachingUserServiceDecorator>();container.decorate<IUserService, LoggingUserServiceDecorator>();// Resolution order: Logging → Caching → UserService // ==============================================// PATTERN 3: MULTIPLE IMPLEMENTATIONS// ==============================================// Multiple implementations of same interface interface INotificationSender { send(message: string, recipient: string): Promise<void>;} class EmailNotificationSender implements INotificationSender { /* ... */ }class SmsNotificationSender implements INotificationSender { /* ... */ }class PushNotificationSender implements INotificationSender { /* ... */ }class SlackNotificationSender implements INotificationSender { /* ... */ } // Register all implementationscontainer.registerMany<INotificationSender>([ { key: "email", implementation: EmailNotificationSender }, { key: "sms", implementation: SmsNotificationSender }, { key: "push", implementation: PushNotificationSender }, { key: "slack", implementation: SlackNotificationSender },]); // Resolve all as collectionclass NotificationService { constructor( private senders: INotificationSender[] // Gets all four ) {} async notifyAll(message: string, recipient: Recipient) { await Promise.all( this.senders.map(s => s.send(message, recipient.id)) ); }} // Or resolve by keyconst emailSender = container.resolveKeyed<INotificationSender>("email"); // ==============================================// PATTERN 4: CONDITIONAL REGISTRATION// ==============================================// Different implementations based on runtime conditions container.register<IPaymentGateway>( Lifecycle.Singleton, (resolver) => { const config = resolver.resolve<IConfiguration>(); const env = config.get("ENVIRONMENT"); switch (config.get("PAYMENT_PROVIDER")) { case "stripe": return new StripeGateway(config.get("STRIPE_KEY")); case "braintree": return new BraintreeGateway(config.get("BRAINTREE_KEY")); case "mock": // Use mock in test/development if (env === "production") { throw new Error("Mock payment gateway not allowed in production"); } return new MockPaymentGateway(); default: throw new Error(`Unknown payment provider`); } }); // ==============================================// PATTERN 5: LAZY RESOLUTION// ==============================================// Defer creation until first use class ExpensiveService { constructor( // Will be created immediately when ExpensiveService is created private alwaysNeeded: IAlwaysUsed, // Will NOT be created until first access private rarelyCalled: Lazy<IRarelyUsed> ) {} doCommonWork() { this.alwaysNeeded.work(); // rarelyCalled is NOT instantiated yet } doRareWork() { // NOW rarelyCalled is instantiated (on first .value access) this.rarelyCalled.value.rareWork(); }} container.register<Lazy<IRarelyUsed>>( Lifecycle.Transient, (resolver) => new Lazy(() => resolver.resolve<IRarelyUsed>())); // ==============================================// PATTERN 6: FACTORY DELEGATES// ==============================================// Inject factory for on-demand creation with parameters interface IConnectionFactory { create(database: string): IDbConnection;} // Via explicit factory interfacecontainer.register<IConnectionFactory, ConnectionFactory>(); class MultiDatabaseService { constructor(private connectionFactory: IConnectionFactory) {} async queryDatabase(databaseName: string) { const connection = this.connectionFactory.create(databaseName); try { return await connection.query("SELECT ..."); } finally { connection.close(); } }} // Or via Func/delegate injectioncontainer.register<Func<string, IDbConnection>>( Lifecycle.Singleton, (resolver) => { return (database: string) => { // Can still access other dependencies const logger = resolver.resolve<ILogger>(); return new PostgresConnection(database, logger); }; });Real applications run in multiple environments: development, testing, staging, production. Each environment may require different implementations, configurations, or behaviors. Properly structured container configuration makes environment switching seamless.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
// ENVIRONMENT-SPECIFIC CONTAINER CONFIGURATION // ==============================================// APPROACH 1: CONDITIONAL MODULES// ============================================== const DevelopmentModule: ContainerModule = { name: "Development", register(container) { // In-memory database for fast tests container.register<IDbContext, InMemoryDbContext>(Lifecycle.Scoped); // Console logging container.register<ILogger, ConsoleLogger>(Lifecycle.Singleton); // Fake external services container.register<IPaymentGateway, FakePaymentGateway>(Lifecycle.Singleton); container.register<IEmailService, ConsoleEmailService>(Lifecycle.Singleton); // Development-specific middleware container.register<IDevMiddleware, RequestTimingMiddleware>(Lifecycle.Singleton); },}; const ProductionModule: ContainerModule = { name: "Production", register(container) { // Real database with connection pooling container.register<IDbContext, PostgresDbContext>(Lifecycle.Scoped); // Structured logging to cloud container.register<ILogger, CloudWatchLogger>(Lifecycle.Singleton); // Real external services container.register<IPaymentGateway, StripeGateway>(Lifecycle.Singleton); container.register<IEmailService, SendGridService>(Lifecycle.Singleton); // Production middleware container.register<IRateLimiter, RedisRateLimiter>(Lifecycle.Singleton); },}; // Composition root selects modules based on environmentasync function bootstrap() { const container = new Container(); const loader = new ModuleLoader(container); const env = process.env.NODE_ENV || "development"; // Core modules always loaded await loader.loadAll([ CoreModule, BusinessModule, ]); // Environment-specific modules if (env === "production") { await loader.load(ProductionModule); } else if (env === "test") { await loader.load(TestModule); } else { await loader.load(DevelopmentModule); } return container;} // ==============================================// APPROACH 2: REGISTRATION OVERRIDES// ============================================== function configureContainer(env: string): Container { const container = new Container(); // Base registrations (always applied) container.register<IUserService, UserService>(Lifecycle.Scoped); container.register<IOrderService, OrderService>(Lifecycle.Scoped); container.register<IValidator, StrictValidator>(Lifecycle.Transient); // Apply environment-specific overrides // Later registrations replace earlier ones for same interface switch (env) { case "production": applyProductionOverrides(container); break; case "staging": applyStagingOverrides(container); break; case "development": applyDevelopmentOverrides(container); break; case "test": applyTestOverrides(container); break; } return container;} function applyProductionOverrides(container: Container) { container.register<ILogger, JsonLogger>(Lifecycle.Singleton); container.register<ICacheClient, RedisClient>(Lifecycle.Singleton); container.register<IPaymentGateway, StripeGateway>(Lifecycle.Singleton);} function applyTestOverrides(container: Container) { container.register<ILogger, NullLogger>(Lifecycle.Singleton); container.register<ICacheClient, InMemoryCache>(Lifecycle.Singleton); container.register<IPaymentGateway, AlwaysSucceedsPaymentGateway>(Lifecycle.Singleton); container.register<IValidator, PermissiveValidator>(Lifecycle.Transient);} // ============================================== // APPROACH 3: CONFIGURATION-DRIVEN REGISTRATION// ============================================== interface ServiceConfiguration { logger: "console" | "json" | "cloudwatch"; database: "postgres" | "mysql" | "inmemory"; cache: "redis" | "memcached" | "inmemory"; payment: "stripe" | "braintree" | "mock";} function configureFromConfig( container: Container, config: ServiceConfiguration) { // Logger selection const loggers = { console: ConsoleLogger, json: JsonLogger, cloudwatch: CloudWatchLogger, }; container.register<ILogger>( Lifecycle.Singleton, () => new loggers[config.logger]() ); // Database selection const databases = { postgres: PostgresDbContext, mysql: MySqlDbContext, inmemory: InMemoryDbContext, }; container.register<IDbContext, typeof databases[config.database]>( Lifecycle.Scoped ); // Similar for other configurable services...} // Configuration loaded from file/environmentconst config = loadConfiguration("config.json");configureFromConfig(container, config);Misconfigured containers cause runtime failures that are notoriously difficult to debug. Missing registrations, circular dependencies, and captive dependencies all manifest as cryptic error messages deep in call stacks. Validation at startup catches these issues before they cause production incidents.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
// CONTAINER VALIDATION AND DIAGNOSTICS // ==============================================// VALIDATION AT STARTUP// ============================================== async function bootstrap() { const container = configureContainer(); // CRITICAL: Validate before running const validationResult = container.validate(); if (!validationResult.isValid) { console.error("Container configuration errors:"); for (const error of validationResult.errors) { console.error(` - ${error.type}: ${error.message}`); if (error.affectedTypes) { console.error(` Affected: ${error.affectedTypes.join(" → ")}`); } } process.exit(1); // Fail fast with clear error } if (validationResult.warnings.length > 0) { console.warn("Container configuration warnings:"); for (const warning of validationResult.warnings) { console.warn(` - ${warning.type}: ${warning.message}`); } } // Proceed only if valid const app = container.resolve<Application>(); await app.start();} // ==============================================// VALIDATION CHECKS TO IMPLEMENT// ============================================== interface ContainerValidator { // Check all constructor dependencies can be resolved validateResolvability(): ValidationResult; // Detect A → B → A cycles detectCircularDependencies(): CircularDependency[]; // Detect singleton → scoped/transient dependencies detectCaptiveDependencies(): CaptiveDependency[]; // Detect multiple registrations for same interface detectAmbiguousRegistrations(): AmbiguousRegistration[]; // Verify disposal is properly configured validateDisposal(): DisposalIssue[]; // Try resolving all registered types smokeTest(): ResolutionError[];} class StandardContainerValidator implements ContainerValidator { constructor(private container: Container) {} validateResolvability(): ValidationResult { const errors: ValidationError[] = []; for (const registration of this.container.getAllRegistrations()) { const constructorParams = this.getConstructorParameters( registration.implementationType ); for (const param of constructorParams) { if (!this.container.hasRegistration(param.type)) { errors.push({ type: "MissingDependency", message: `${registration.serviceType.name} requires ${param.type.name} which is not registered`, affectedTypes: [registration.serviceType.name, param.type.name], }); } } } return { isValid: errors.length === 0, errors }; } detectCaptiveDependencies(): CaptiveDependency[] { const issues: CaptiveDependency[] = []; for (const registration of this.container.getAllRegistrations()) { if (registration.lifecycle !== Lifecycle.Singleton) continue; const dependencies = this.getDependencyGraph(registration.serviceType); for (const dep of dependencies) { const depRegistration = this.container.getRegistration(dep); if (depRegistration?.lifecycle === Lifecycle.Scoped) { issues.push({ captorType: registration.serviceType.name, captorLifecycle: "Singleton", captiveType: dep.name, captiveLifecycle: "Scoped", message: `Singleton "${registration.serviceType.name}" captures Scoped "${dep.name}". The scoped instance will outlive its intended scope.`, }); } } } return issues; } smokeTest(): ResolutionError[] { const errors: ResolutionError[] = []; for (const registration of this.container.getAllRegistrations()) { try { // Actually try to create each registered type using scope = this.container.createScope(); scope.resolve(registration.serviceType); } catch (error) { errors.push({ serviceType: registration.serviceType.name, error: error.message, stack: error.stack, }); } } return errors; }} // ==============================================// DIAGNOSTIC OUTPUT// ============================================== function printContainerDiagnostics(container: Container) { console.log("=== Container Diagnostics ==="); // Registration summary const registrations = container.getAllRegistrations(); console.log(`Total Registrations: ${registrations.length}`); const byLifecycle = groupBy(registrations, r => r.lifecycle); console.log("By Lifecycle:"); console.log(` Singleton: ${byLifecycle[Lifecycle.Singleton]?.length ?? 0}`); console.log(` Scoped: ${byLifecycle[Lifecycle.Scoped]?.length ?? 0}`); console.log(` Transient: ${byLifecycle[Lifecycle.Transient]?.length ?? 0}`); // Dependency graph visualization console.log("Dependency Graph:"); for (const reg of registrations) { const deps = container.getDependencies(reg.serviceType); if (deps.length > 0) { console.log(` ${reg.serviceType.name} → ${deps.map(d => d.name).join(", ")}`); } }} // Output example:// === Container Diagnostics ===// // Total Registrations: 47// // By Lifecycle:// Singleton: 12// Scoped: 28// Transient: 7// // Dependency Graph:// UserService → IUserRepository, ILogger, ICache// OrderService → IOrderRepository, IUserService, IPaymentGateway// ...Always validate container configuration at application startup in production. A missing registration discovered 3 AM during a traffic spike is exponentially more expensive than one caught during deployment. The slight startup time cost of validation is negligible compared to runtime failures.
Container configuration establishes the architectural backbone of your application. Following best practices ensures maintainability as the application evolves.
Container configuration is where architectural intentions become executable reality. We've covered the essential knowledge for production-quality configuration:
What's Next:
With registration and configuration mastered, the next page explores automatic dependency resolution—how containers traverse dependency graphs, select constructors, handle complex resolution scenarios, and optimize resolution performance.
You now understand container configuration comprehensively: registration fundamentals, explicit vs convention-based strategies, modular organization, advanced patterns, environment handling, and validation. Next, we'll explore how containers automatically resolve dependencies from these configurations.