Runtime Selection
Choose Implementations at Runtime
Most classes should inject their collaborators directly. Reach for the scoped container only when a runtime input decides which implementation class should handle the work.
Default: Direct Injection
If the dependency has one implementation, inject that implementation or abstract contract directly. The container resolves everything automatically.
@Component({ scope: LoadAs.Singleton })
export class CheckoutService {
constructor(
@Inject(PriceCalculator) private prices: PriceCalculator,
) {}
}The Problem: Runtime Implementation Choice
Sometimes you can't know which implementation class to use until a user makes a choice — export format (PDF vs CSV), payment provider, storage backend. You can't inject every possible implementation at construction time, and you shouldn't import them all just to switch on a string.
Avoid this anti-pattern: Injecting all implementations and switching on a parameter leads to tight coupling, wasted construction cost, and code that needs updating every time a new variant is added.
The Solution: SCOPED_CONTAINER
Inject SCOPED_CONTAINER — the container for the current scope — choose the implementation class token, and resolve that class lazily. The container handles construction, lifetime management, and dependency resolution for the selected class.
import {
Component,
Inject,
LoadAs,
SCOPED_CONTAINER,
type IContainer,
} from "@noego/ioc";
abstract class ExportRuntime {
abstract export(data: ReportData): Promise<Buffer>;
}
@Component({ scope: LoadAs.Singleton })
class PdfExportRuntime extends ExportRuntime {
async export(data: ReportData): Promise<Buffer> {
// PDF implementation.
}
}
@Component({ scope: LoadAs.Singleton })
class CsvExportRuntime extends ExportRuntime {
async export(data: ReportData): Promise<Buffer> {
// CSV implementation.
}
}
@Component({ scope: LoadAs.Singleton })
export class ReportExporter {
constructor(
@Inject(SCOPED_CONTAINER) private container: IContainer,
) {}
async export(format: "pdf" | "csv", data: ReportData): Promise<Buffer> {
const Runtime = format === "pdf" ? PdfExportRuntime : CsvExportRuntime;
const runtime = await this.container.instance(Runtime);
return runtime.export(data);
}
}Rules
- Use direct constructor injection when there is one implementation.
- Use SCOPED_CONTAINER only when runtime data selects the implementation class.
- Select class tokens, not strings that get switched again later.
- Keep selection code thin; business logic belongs in the selected implementation.
- Do not introduce Provider classes just to wrap container.instance(...).