Runtime Selection

Runtime Selection

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.

Direct Injection

If the dependency has one implementation, inject that implementation or abstract contract directly. The container resolves everything automatically.

checkout_service.ts
@Component({ scope: LoadAs.Singleton })
export class CheckoutService {
  constructor(
    @Inject(PriceCalculator) private prices: PriceCalculator,
  ) {}
}

Runtime 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.

Use 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.

report_exporter.ts
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(...).
NoEgo

© 2025 NoEgo. All rights reserved.