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:

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

  1. Prefer Interfaces: Use DI.createInterface for public contracts

  2. Use Appropriate Lifecycles: Singleton for stateless, transient for stateful

  3. Leverage Child Containers: Isolate feature/route dependencies

  4. Type Your Resolvers: Use resolve<IFactoryResolver<T>> for type safety

  5. Document Resolvers: Explain why you're using lazy, optional, etc.

  6. Clean Up: Dispose child containers when features unmount

  7. Test with Mocks: Use Registration.instance to inject test doubles

  8. Avoid 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?