Advanced DI Patterns & Recipes
Aurelia's dependency injection system is powerful yet lightweight. This guide explores advanced patterns verified against the framework's codebase, showing you how to leverage DI for sophisticated application architecture.
Prerequisites
Before diving into advanced patterns, ensure you're familiar with:
Using
resolve()from@aurelia/kernel
Table of Contents
Interface-Based DI
Creating Interfaces with DI.createInterface
Interfaces allow you to inject by contract rather than concrete implementation, enabling better testability and flexibility.
Default Registration
Provide a default implementation directly in the interface:
Now any component can inject ILogger without explicit registration:
Override Default Implementation
Replace the default when needed:
Real Example: Fetch Function Interface
From @aurelia/fetch-client:
Aliasing Interfaces
Registration Patterns
Aurelia provides several registration helpers for different lifecycle needs.
Registration.instance
Register a pre-created instance:
Registration.singleton
Create and cache one instance per container:
Registration.transient
Create a new instance on every resolution:
Registration.callback
Execute a function each time the key is resolved:
Registration.cachedCallback
Execute callback once, then cache the result:
Real-world example from @aurelia/router:
Registration.aliasTo
Create multiple keys that resolve to the same instance:
Resolver Patterns
Resolvers modify how dependencies are injected, enabling advanced patterns.
lazy: Deferred Resolution
Inject a function that resolves the dependency when called:
Type-safe usage:
optional: Graceful Fallbacks
Inject undefined if dependency not registered:
all: Collect All Registrations
Resolve all registered instances of a key:
With searchAncestors:
factory: Dynamic Instance Creation
Inject a factory function that creates new instances:
Type-safe factory:
Real example from @aurelia/fetch-client:
newInstanceOf: Always New Instance
Create a new instance every time, ignoring registration lifecycle:
newInstanceForScope: Scoped Instances
Create one instance per requesting container and register it there:
Why use newInstanceForScope?
Validation controllers are scoped to components
Each form gets its own controller
Child components can inject the same controller
Automatic cleanup when component is disposed
own: Container-Local Resolution
Only resolve if the dependency is registered in the requesting container (not ancestors):
resource & optionalResource: Smart Resolution
Resolve from requestor or root, skipping intermediate containers:
Factory Patterns
Service Factory Pattern
Create services dynamically based on configuration:
Plugin Factory Pattern
Dynamically register and create plugins:
Child Containers & Scoping
Child containers enable hierarchical dependency scoping—perfect for features, routes, or components with isolated dependencies.
Creating Child Containers
Inherit Parent Resources
Real example from @aurelia/router:
Scoped Service Pattern
Feature Module Pattern
Transformers
Transformers modify instances after construction—useful for decoration, proxying, or post-processing.
Registering Transformers
Multiple Transformers
Transformers execute in registration order:
Real-World: Adding Lifecycle Hooks
Real-World Recipes
Recipe 1: Multi-Tenant Application
Recipe 2: Environment-Based Configuration
Recipe 3: Plugin System with DI
Recipe 4: Decorator Pattern with Transformers
Recipe 5: Context-Aware Services
Best Practices
Prefer Interfaces: Use
DI.createInterfacefor public contractsUse Appropriate Lifecycles: Singleton for stateless, transient for stateful
Leverage Child Containers: Isolate feature/route dependencies
Type Your Resolvers: Use
resolve<IFactoryResolver<T>>for type safetyDocument Resolvers: Explain why you're using lazy, optional, etc.
Clean Up: Dispose child containers when features unmount
Test with Mocks: Use
Registration.instanceto inject test doublesAvoid Service Locator: Prefer constructor injection via
resolve()
Common Pitfalls
Pitfall 1: Circular Dependencies
Pitfall 2: Forgetting Child Container Disposal
Pitfall 3: Over-using Transformers
Transformers run on every resolution. Don't use them for expensive operations that should run once:
Conclusion
Aurelia's DI system provides powerful primitives for building scalable, maintainable applications. By mastering interfaces, registration patterns, resolvers, and child containers, you can architect sophisticated dependency graphs that remain testable and flexible.
Key takeaways:
Use interfaces for flexibility and testing
Choose the right registration lifecycle
Leverage resolvers for advanced scenarios
Scope dependencies with child containers
Apply transformers judiciously
For more information:
Last updated
Was this helpful?