A professional-grade dependency injection container for TypeScript
4 hours ago
1
A professional-grade dependency injection container for TypeScript applications built with SOLID principles.
This is not a toy container for simple dependency wiring. @webiny/di is engineered for architects and senior developers who build complex, maintainable systems using Clean Architecture, Domain-Driven Design, and rigorous adherence to SOLID principles.
⚠️ Prerequisites: This library assumes you understand and practice:
Dependency Inversion Principle - Programming to abstractions, not implementations
Open/Closed Principle - Extending behavior through decoration, not modification
✨ True Type Safety: Unlike other DI containers, type safety is enforced at compile-time. The Abstraction<T> class unifies tokens and types, making it impossible to resolve dependencies with incorrect types. No manual generic passing, no runtime surprises.
🎯 First-Class Decorator Pattern: The only DI container with true abstraction decoration. Implement the Open/Closed Principle without compromise - extend behavior through decoration, not modification.
🏛️ SOLID by Design: Every feature exists to enable rigorous adherence to SOLID principles. This isn't a general-purpose container - it's a tool for professional architects building maintainable systems.
🔗 Composite Pattern: Register multiple implementations as composites for elegant handling of collections
🏗️ Hierarchical Containers: Create child containers with inheritance for scoped lifetimes and feature isolation
⚡ Lifetime Scopes: Transient and Singleton lifetime management
🔍 Metadata-Based: Uses reflect-metadata for clean, decorator-free API
🎨 Clean Architecture Ready: Designed from the ground up for Clean Architecture and Domain-Driven Design
npm install @webiny/di reflect-metadata
Note: This package requires reflect-metadata to be installed and imported at your application's entry point.
@webiny/di is for you if:
✅ You build applications using Clean Architecture or Hexagonal Architecture
✅ You practice Domain-Driven Design
✅ You understand and apply SOLID principles rigorously
✅ You need to extend system behavior through composition, not modification
✅ You value compile-time safety over runtime flexibility
✅ You're building enterprise applications where maintainability is critical
This library is NOT for you if:
❌ You're looking for a quick way to wire up dependencies in a simple app
❌ Your codebase uses concrete classes everywhere (no abstractions)
❌ You prefer runtime configuration over compile-time safety
❌ You're building a prototype or MVP where architecture doesn't matter yet
import"reflect-metadata";import{Container,Abstraction,createImplementation}from"@webiny/di";// 1. Define an abstraction (interface token)interfaceIUserRepository{getById(id: string): Promise<User>;}constUserRepository=newAbstraction<IUserRepository>("UserRepository");// 2. Create an implementationclassUserRepositoryImplimplementsIUserRepository{asyncgetById(id: string): Promise<User>{// implementation}}constUserRepositoryImplementation=createImplementation({abstraction: UserRepository,implementation: UserRepositoryImpl,dependencies: []});// 3. Register and resolveconstcontainer=newContainer();container.register(UserRepositoryImplementation);constuserRepo=container.resolve(UserRepository);
Abstractions are type-safe tokens that represent interfaces or abstract contracts.
The container provides enforced type safety through the Abstraction<T> class. Unlike other DI containers where tokens and types are separate, here they are unified:
// The abstraction IS the token AND carries the typeconstLogger=newAbstraction<ILogger>("Logger");// ✅ Type is automatically inferred from the abstractionconstlogger=container.resolve(Logger);// Type: ILogger (no manual generic needed!)// ✅ Dependencies are type-checked against constructor parametersconstUserServiceImpl=createImplementation({abstraction: UserService,implementation: UserServiceImpl,dependencies: [UserRepository,// Type-checked: must be IUserRepositoryLogger// Type-checked: must be ILogger]});// ❌ TypeScript error: wrong typeconstwrong: ISomeOtherType=container.resolve(Logger);// Error: Type 'ILogger' is not assignable to type 'ISomeOtherType'
This approach eliminates entire classes of bugs that exist in other DI containers where you can accidentally resolve the wrong type.
Here's a complete example of a feature using clean architecture principles:
// Stateful services should be singletonscontainer.register(CacheImpl).inSingletonScope();container.register(DatabaseConnectionImpl).inSingletonScope();// Stateless services can be transientcontainer.register(GetUserUseCaseImpl);// transient by default
5. Prefer Constructor Injection
// ✅ Good: Constructor injectionclassUserService{constructor(privaterepository: IUserRepository){}}// ❌ Avoid: Property injection or service locator patternclassUserService{repository?: IUserRepository;setRepository(repo: IUserRepository){this.repository=repo;}}
The container provides clear error messages:
// Circular dependency detectiontry{container.resolve(ServiceA);}catch(error){// Error: Circular dependency detected for ServiceA}// Missing registrationtry{container.resolve(UnregisteredService);}catch(error){// Error: No registration found for UnregisteredService}// Missing abstraction metadatatry{container.register(ImplementationWithoutMetadata);}catch(error){// Error: No abstraction metadata found for ImplementationWithoutMetadata}
import{describe,it,expect,vi}from"vitest";describe("GetUserUseCase",()=>{it("should fetch user by id",async()=>{constmockRepository: IUserRepository={getById: vi.fn().mockResolvedValue({id: "1",name: "John"}),save: vi.fn()};constuseCase=newGetUserUseCaseImpl(mockRepository);constuser=awaituseCase.execute("1");expect(user?.name).toBe("John");expect(mockRepository.getById).toHaveBeenCalledWith("1");});});
Integration Testing with Container
import{describe,it,expect,beforeEach}from"vitest";describe("User Feature Integration",()=>{letcontainer: Container;beforeEach(()=>{container=newContainer();registerUserFeature(container);});it("should wire up all dependencies correctly",()=>{constuseCase=container.resolve(GetUserUseCase);expect(useCase).toBeDefined();});it("should execute use case with real dependencies",async()=>{constuseCase=container.resolve(GetUserUseCase);constuser=awaituseCase.execute("123");expect(user).toBeDefined();});});
Testing with Child Containers
describe("User Feature with Overrides",()=>{it("should use mock repository in tests",async()=>{constparentContainer=newContainer();registerUserFeature(parentContainer);consttestContainer=parentContainer.createChildContainer();// Override repository with mockconstmockRepo=newMockUserRepository();testContainer.registerInstance(UserRepository,mockRepo);constuseCase=testContainer.resolve(GetUserUseCase);constuser=awaituseCase.execute("123");expect(mockRepo.getByIdCalled).toBe(true);});});
Performance Considerations
Singleton Scope: Use for expensive-to-create objects (database connections, caches)
Transient Scope: Use for lightweight, stateless services
Resolution Caching: Singleton instances are cached automatically
Decorator Overhead: Minimal, but avoid excessive decorator chains (>10)
Built for SOLID Principles, Not Just Convenience
Most DI containers are designed to make dependency wiring convenient. @webiny/di goes further - it's engineered to make SOLID principles not just possible, but natural and frictionless.
The truth about DI containers: They provide architectural value only when used with SOLID principles. Without abstractions (Dependency Inversion Principle) and without the need to extend behavior (Open/Closed Principle), a DI container is just glorified object instantiation.
@webiny/di doesn't pretend otherwise. This library is for professionals building:
Clean Architecture applications with clear layer boundaries
Domain-Driven Design systems with rich domain models
Extensible platforms where behavior is composed, not hardcoded
Enterprise applications where maintainability matters more than quick hacks
Most DI containers claim to be "type-safe," but they rely on manual generic parameters that can be incorrect. In these libraries, the token (string/symbol) and the type (generic parameter) are separate, allowing for mismatches:
// ❌ Other libraries - token and type are separateconstTYPES={UserRepository: Symbol.for("UserRepository")};container.bind<IUserRepository>(TYPES.UserRepository).to(UserRepositoryImpl);// ❌ You can resolve with the WRONG type - compiles fine, fails at runtime!constrepo=container.get<IProductRepository>(TYPES.UserRepository);// TypeScript can't catch this mistake because token and type are disconnected
@webiny/di solves this by unifying tokens and types in the Abstraction<T> class:
// ✅ Token and type are unified - impossible to mess upconstUserRepository=newAbstraction<IUserRepository>("UserRepository");// ✅ Type is automatically enforcedconstrepo=container.resolve(UserRepository);// Always returns IUserRepository// ✅ Wrong type assignment caught at compile-timeconstwrong: IProductRepository=container.resolve(UserRepository);// TypeScript error: Type 'IUserRepository' is not assignable to type 'IProductRepository'
Key benefits:
No manual generic passing - types are automatic
Compile-time verification of all dependencies
Impossible to resolve with incorrect types
Refactoring-safe - rename interfaces and all usages update
First-Class Decorator Pattern
@webiny/di is the only DI container that implements true decoration of abstractions, making the Open/Closed Principle practical and natural.
"Software entities should be open for extension, but closed for modification." - Bertrand Meyer
This library makes it the default way of working. Decorators are registered on the abstraction, not on concrete implementations:
// ✅ Register base implementation - closed for modificationcontainer.register(PageRepositoryImpl);// ✅ Extend through decoration - open for extensioncontainer.registerDecorator(CachingDecorator);container.registerDecorator(LoggingDecorator);container.registerDecorator(MetricsDecorator);// ✅ All decorators automatically applied in orderconstrepo=container.resolve(PageRepository);// Returns: MetricsDecorator -> LoggingDecorator -> CachingDecorator -> PageRepositoryImpl
This is the Open/Closed Principle in practice:
Base implementation is closed for modification - it never changes
Behavior is open for extension - add decorators without touching core code
Decorators are registered separately from implementations
Third-party code can extend behavior by simply registering decorators
Real-World Example: Extensible Systems
// Core application - defines abstractions and base implementationscontainer.register(CreatePageUseCaseImpl);// ✅ Team member adds validation - extends via decorationcontainer.registerDecorator(ValidationDecorator);// ✅ Team member adds authorization - extends via decorationcontainer.registerDecorator(AuthorizationDecorator);// ✅ DevOps adds metrics - extends via decorationcontainer.registerDecorator(MetricsDecorator);// ✅ Customer adds custom business rules - extends via decorationcontainer.registerDecorator(CustomBusinessRulesDecorator);// All decorators automatically compose!constuseCase=container.resolve(CreatePageUseCase);// Execution flow:// CustomBusinessRulesDecorator// -> MetricsDecorator// -> AuthorizationDecorator// -> ValidationDecorator// -> CreatePageUseCaseImpl// Original CreatePageUseCaseImpl was NEVER modified - Open/Closed Principle achieved.
This pattern is fundamental to:
Clean Architecture - Use cases decorated with cross-cutting concerns
Domain-Driven Design - Domain services enhanced with infrastructure concerns
Hexagonal Architecture - Ports decorated with adapters
CQRS - Command handlers decorated with validation, authorization, auditing
Only @webiny/di makes this pattern first-class. In other containers, you're fighting the framework. Here, you're working with it.
Comparison with Other Containers
@webiny/di isn't trying to be a general-purpose DI container. It's optimized for one thing: making SOLID principles practical in TypeScript applications.
Here's how it compares to other popular containers:
Feature
@webiny/di
InversifyJS
TSyringe
Type Safety
✅ Enforced (Token = Type)
⚠️ Manual (Token ≠ Type)
⚠️ Manual (Token ≠ Type)
Wrong Type Resolution
✅ Compile error
❌ Runtime error
❌ Runtime error
Decorator Pattern
✅ First-class (abstraction decoration)
❌ Manual chaining only
❌ Manual chaining only
Open/Closed Principle
✅ Native support
⚠️ Manual implementation
⚠️ Manual implementation
Composites
✅ Built-in
❌ No
❌ No
Child Containers
✅ Yes
✅ Yes
✅ Yes
Metadata
✅ reflect-metadata
✅ reflect-metadata
✅ reflect-metadata
Target Audience
Professional architects
General purpose
General purpose
Learning Curve
Low (if you know SOLID)
Medium
Low
Bottom line: If you're building with SOLID principles, @webiny/di removes friction. If you're not, other containers might be more flexible for your use case.
@webiny/di is opinionated by design. It embodies a specific philosophy about software architecture:
What people call "plugins" are simply:
Implementations of abstractions defined by the core system
Decorations of abstractions to extend behavior
Composites that collect multiple implementations
DI Containers Are Only Truly Useful with SOLID
A DI container without SOLID principles is like:
A type system without types
A database without queries
A compiler without syntax checking
You can use it, but you're missing the entire point.
Professional Tools for Professional Developers
This library doesn't try to be everything to everyone. It's a professional tool for developers who:
Understand that architecture matters!
Know that maintainability is more valuable than initial simplicity
Accept that good design requires discipline and expertise
If you're building a throwaway prototype, use something simpler. If you're building a system that will live for years and be maintained by multiple teams, @webiny/di will pay dividends.
We Choose Compile-Time Safety Over Runtime Flexibility
Some DI containers let you do anything at runtime. This library intentionally restricts you to compile-time verified operations.
Why? Because runtime errors in production are expensive. Compile-time errors are free.
We'd rather you hit a TypeScript error during development than a runtime error at 3 AM.