Prefer property/field injection or want to avoid decorators
Cleaner syntax, works with inheritance, no metadata required
@inject
Prefer explicit constructor injection and immutable dependencies
Constructor clearly documents required services; great for unit tests
static inject
Avoiding decorators entirely
No decorator metadata required
Choosing your style:
Use resolve() when you want lightweight property injection, when inheriting from framework base classes, or when decorators/emitDecoratorMetadata are unavailable.
Use @inject when you want constructor parameters to stay read-only, when your team prefers explicit signatures, or when you need to support tooling that analyzes constructor arguments.
static inject remains available for teams that disable decorators entirely.
Heads up: default implementations registered inside DI.createInterface are only consulted when the container has registered the token itself. Resolvers such as optional(IUserService) or resolve(all(IUserService)) will return undefined until you run container.register(IUserService). This matches the runtime behavior in packages/kernel/src/di.ts (container.register(MyStr); comment) and avoids surprising allocations when optional dependencies are missing.
You can also resolve multiple keys in one call and destructure the tuple result:
This uses the runtime helper defined in packages/kernel/src/di.container.ts to pull each token from the currently active container, which keeps the code concise when a class needs several collaborators.
Registration exposes more than singleton vs transient (see packages/kernel/src/di.registration.ts). Pick the helper that matches your lifetime and creation strategy:
Helper
What it does
Typical use
Registration.instance(key, value)
Always returns the provided object.
App configuration, external SDK singletons.
Registration.singleton(key, Type)
Lazily creates one instance per container.
Services with shared state (API clients, stores).
Registration.transient(key, Type)
Creates a new instance every time.
Utilities or disposable types.
Registration.callback(key, fn)
Runs the callback on every resolution.
Values that depend on runtime arguments or container state.
Registration.cachedCallback(key, fn)
Runs the callback once per container then caches the result.
Expensive factories that still need manual control of construction.
Registration.aliasTo(original, alias)
Exposes an existing registration under another key.
Provide the same implementation for multiple tokens (mock vs real).
Registration.defer(extension, data)
Defers resource registration until a dedicated registry handles it.
Template preprocessors and conventions (used by the HTML preprocessor for CSS modules).
Combine these helpers with Aurelia.register(...) or container-local register(...) calls wherever you wire up services.
Use InstanceProvider when you need to expose a value that should be resolved exactly as-is by descendants. Aurelia uses the same primitive to wire controllers, hydration contexts, and router state into child scopes (see packages/runtime-html/src/templating/controller.ts and packages/router/src/route-context.ts).
Call registerResolver with the provider so resolve(IFeatureContext) returns the prepared value.
The optional third argument (true) tells the container to dispose the provider automatically when the scope goes away.
You can replace the value later via provider.prepare(newValue) to update the scoped instance.
How do I plug in custom factories or transformers?
When the built-in lifetime helpers are not enough, register your own factory for a key. This is how Aurelia supports interface tokens whose concrete type needs extra inputs (see packages/tests/src/1-kernel/di.get.spec.ts for working examples).
container.registerFactory(key, factory) ties a token to a custom factory. For interface tokens you can cast the interface to Constructable exactly like the runtime tests do. Inside construct you can call container.getFactory(SomeClass).construct(...) to reuse Aurelia's dependency calculation.
container.registerTransformer(key, transformer) lets you wrap or mutate instances after construction—perfect for logging proxies, caching, or feature flags. The container implementation keeps the transformer list per key (packages/kernel/src/di.container.ts:305-330, 664-668).
CachedReportService in the example is any decorator you want to apply—it simply receives the just-created ReportService and returns the wrapped instance you want the container to hand out.
If you simply need one-off construction hooks, prefer Registration.callback/Registration.cachedCallback. Reach for registerFactory only when you need full control over how and when instances are created.
Container Management
How do I configure a container?
DI.createContainer() accepts an optional configuration object that maps directly to the runtime IContainerConfiguration (packages/kernel/src/di.container.ts). Use it to change inheritance and default registration strategy:
inheritParentResources copies the parent container’s resource registrations (custom elements, attributes, value converters, etc.) into the child. Shadow DOM features or micro-frontends can opt in so they see exactly what the parent registered without falling back to the app root.
defaultResolver controls how plain classes are auto-registered when you first resolve them. DefaultResolver.singleton (the default) caches one instance per container; switching to DefaultResolver.transient ensures every resolve(SomeClass) call returns a fresh instance. If you want to force explicit registrations, use DefaultResolver.none so the container throws whenever you resolve an unknown class (great for large teams that prefer auditability).
Child containers can also pass { inheritParentResources: true } to createChild(...) for one-off scopes that need the same behavior.
How do I check or override an existing registration?
The container exposes inspection APIs so you can detect whether something is registered and optionally swap it out at runtime (see IContainer.has/getResolver in packages/kernel/src/di.ts).
container.has(key, searchAncestors) lets you check whether a key exists locally or anywhere up the parent chain.
container.getResolver(key, /*autoRegister*/ false) gives you the current resolver without triggering auto-registration, so you can inspect or replace it.
registerResolver accepts any IResolver (including InstanceProvider) and an optional isDisposable flag to clean up automatically.
How do I deregister a service?
Warning: Use sparingly - can cause issues if other services depend on it.
import { resolve } from '@aurelia/kernel';
import { IUserService } from './user-service';
export class UserList {
private userService = resolve(IUserService);
async attached() {
const users = await this.userService.getUsers();
}
}
import { inject } from '@aurelia/kernel';
import { IUserService } from './user-service';
@inject(IUserService)
export class UserList {
constructor(private userService: IUserService) {}
}
export class MyComponent {
private api = resolve(IApiClient);
private logger = resolve(ILogger);
}
import { DI } from '@aurelia/kernel';
// 1. Create the service class
export class UserService {
async getUsers(): Promise<User[]> {
// implementation
}
}
// 2. Create the interface token
export const IUserService = DI.createInterface<IUserService>(
'IUserService',
x => x.singleton(UserService) // Auto-register as singleton
);
// 3. Export the type for consumers
export type IUserService = UserService;
import { resolve, optional } from '@aurelia/kernel';
export class AnalyticsComponent {
// Service might not be registered - won't throw error
private analytics = resolve(optional(IAnalyticsService));
trackEvent(name: string) {
if (this.analytics) {
this.analytics.track(name);
}
}
}
import { inject, optional } from '@aurelia/kernel';
@inject(optional(IAnalyticsService))
export class AnalyticsComponent {
constructor(private analytics?: IAnalyticsService) {}
}
import { resolve, all } from '@aurelia/kernel';
export class PluginManager {
// Get array of all registered plugins
private plugins = resolve(all(IPlugin));
initializePlugins() {
this.plugins.forEach(plugin => plugin.initialize());
}
}
import { resolve, lazy } from '@aurelia/kernel';
export class ReportGenerator {
// Service won't be created until called
private getHeavyService = resolve(lazy(IHeavyProcessingService));
async generateReport() {
const service = this.getHeavyService(); // Created here
return await service.process();
}
}
import { DI } from '@aurelia/kernel';
export class PaymentService {
// Implementation
}
// Create token WITHOUT default registration
export const IPaymentService = DI.createInterface<IPaymentService>('IPaymentService');
export type IPaymentService = PaymentService;
import { Registration } from '@aurelia/kernel';
// In main.ts or feature registration
Aurelia.register(
Registration.singleton(IPaymentService, PaymentService)
);
// No interface needed - use class directly
export class LoggerService {
log(message: string) {
console.log(message);
}
}
// Inject using the class
import { resolve } from '@aurelia/kernel';
export class MyComponent {
private logger = resolve(LoggerService);
}
import { DI } from '@aurelia/kernel';
export class AuthService {
private currentUser: User | null = null;
isAuthenticated(): boolean {
return this.currentUser !== null;
}
}
// Singleton (default)
export const IAuthService = DI.createInterface<IAuthService>(
'IAuthService',
x => x.singleton(AuthService)
);
export type IAuthService = AuthService;
import { DI } from '@aurelia/kernel';
export class EventLogger {
private readonly timestamp = new Date();
log(event: string) {
console.log(`[${this.timestamp.toISOString()}] ${event}`);
}
}
// Transient - new instance each time
export const IEventLogger = DI.createInterface<IEventLogger>(
'IEventLogger',
x => x.transient(EventLogger)
);
export type IEventLogger = EventLogger;
import { resolve, newInstanceOf } from '@aurelia/kernel';
export class ReportGenerator {
// Always creates a fresh instance
private processor = resolve(newInstanceOf(DataProcessor));
}
import { inject, newInstanceOf } from '@aurelia/kernel';
@inject(newInstanceOf(DataProcessor))
export class ReportGenerator {
constructor(private processor: DataProcessor) {}
}
import { resolve, newInstanceForScope } from '@aurelia/kernel';
export class FormComponent {
// Each form component gets its own validation service
private validator = resolve(newInstanceForScope(IValidationService));
}
import { resolve, last } from '@aurelia/kernel';
export class ConfigLoader {
// Get the most recently registered config
private config = resolve(last(IAppConfig));
}
container.register(Registration.instance(IAppConfig, defaultConfig));
container.register(Registration.instance(IAppConfig, customConfig)); // This one wins with last()
import { Constructable, DI, IContainer, Registration, resolve } from '@aurelia/kernel';
export interface ReportService {
run(reportId: string): Promise<Response>;
}
export const IReportService = DI.createInterface<ReportService>('IReportService');
export interface ReportConfig { endpoint: string; }
export const IReportConfig = DI.createInterface<ReportConfig>('IReportConfig');
class ReportServiceImpl implements ReportService {
constructor(private readonly api = resolve(IApiClient)) {}
run(reportId: string) {
return this.api.get(`/reports/${reportId}`);
}
}
const reportFactory = {
Type: ReportServiceImpl,
transformers: [] as ((instance: ReportServiceImpl) => ReportServiceImpl)[],
construct(container: IContainer) {
const instance = container.getFactory(ReportServiceImpl).construct(container);
const config = container.get(IReportConfig);
instance.setBaseUrl(config.endpoint);
return this.transformers.reduce((inst, transform) => transform(inst), instance);
},
registerTransformer(transformer: (instance: ReportServiceImpl) => ReportServiceImpl) {
this.transformers.push(transformer);
}
};
const container = DI.createContainer();
container.registerFactory(IReportService as unknown as Constructable, reportFactory);
container.register(
Registration.instance(IReportConfig, { endpoint: '/api/reporting' })
);
// Later you can decorate every resolved instance
container.registerTransformer(
IReportService as unknown as Constructable,
report => new CachedReportService(report) // Your decorator implementation
);
import { DI, InstanceProvider } from '@aurelia/kernel';
import { ICacheService } from './cache-service';
const container = DI.createContainer();
if (!container.has(ICacheService, true)) {
console.warn('Cache service missing, falling back to noop implementation.');
}
const resolver = container.getResolver(ICacheService, false);
if (resolver) {
// Override the resolver for the current container (useful in tests)
container.registerResolver(
ICacheService,
new InstanceProvider('ICacheService', new FakeCacheService()),
true
);
}
import { DI } from '@aurelia/kernel';
const container = DI.createContainer();
// Register
container.register(Registration.singleton(IService, Service));
// Later, deregister
container.deregister(IService);
// If inference fails
private service: IUserService = resolve(IUserService);
// Or use type assertion
private service = resolve<IUserService>(IUserService);
// ✓ Singleton
export const IService = DI.createInterface<IService>(
'IService',
x => x.singleton(Service)
);
// ✗ Transient (creates new instance each time)
export const IService = DI.createInterface<IService>(
'IService',
x => x.transient(Service)
);
// ✗ BAD: Bypasses DI
const service = new Service();
// ✓ GOOD: Get from container
const service = resolve(IService);
// ✗ BAD: Order mismatch
@inject(IServiceB, IServiceA)
export class MyClass {
constructor(
private serviceA: IServiceA, // Wrong! Gets ServiceB
private serviceB: IServiceB // Wrong! Gets ServiceA
) {}
}
// ✓ GOOD: Order matches
@inject(IServiceA, IServiceB)
export class MyClass {
constructor(
private serviceA: IServiceA,
private serviceB: IServiceB
) {}
}
// ✓ BETTER: Use resolve() to avoid this issue
export class MyClass {
private serviceA = resolve(IServiceA);
private serviceB = resolve(IServiceB);
}