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.

checkout_service.ts
@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.

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.