Resolution

Nested Resolution and Lifetimes

Nested resolution is what happens after the container starts building an object graph. Runtime parameters and scoped instances flow through that graph, while singletons continue to represent process-level collaborators that can be safely shared.

Runtime Context Propagates Downward

Create a child scope at the entry point, then pass runtime values into the resolution call. Services deeper in the graph can inject those values without each intermediate service forwarding them manually.

import createContainer, { Component, Inject, LoadAs, Parameter } from "@noego/ioc";

export const USER_ID = Parameter.create("userId");
export const TENANT_ID = Parameter.create("tenantId");

@Component({ scope: LoadAs.Singleton })
export class CsvFormatter {
  format(rows: string[][]): string {
    return rows.map((row) => row.join(",")).join("\n");
  }
}

@Component({ scope: LoadAs.Scoped })
export class ReportContext {
  constructor(
    @Inject(USER_ID) private userId: string,
    @Inject(TENANT_ID) private tenantId: string
  ) {}

  heading(): string {
    return `Report for ${this.tenantId}/${this.userId}`;
  }
}

@Component({ scope: LoadAs.Scoped })
export class ReportService {
  constructor(
    @Inject(ReportContext) private context: ReportContext,
    @Inject(CsvFormatter) private csv: CsvFormatter
  ) {}

  generate(): string {
    return this.csv.format([[this.context.heading()]]);
  }
}

const container = createContainer();
const scope = container.extend();

const report = await scope.instance(ReportService, [
  USER_ID.value("123"),
  TENANT_ID.value("enterprise-a")
]);

The important rule is that values are supplied to scope.instance(...) or scope.get(...) with USER_ID.value(...) and other parameter-token values. The child scope is created with container.extend(); it does not receive parameter values directly.

Where Singletons Fit

Singleton does not mean "only stateless code." It means one instance is shared for the life of the container. That is correct for stable application collaborators and wrong for data that changes per resolution.

Stateless behavior

Formatters, calculators, validators, policy checks, and orchestration services are good singleton candidates when their output depends only on method input and injected stable collaborators.

Shared resources

Database runners, connection pools, process managers, event buses, and caches can be singletons because the whole application intentionally coordinates through one shared resource.

Stable global state

Application configuration, feature policy readers, and immutable runtime settings can be singletons when they represent process-level state rather than request-level state.

In a nested resolution graph, singleton dependencies can be injected into scoped services freely. The risky direction is the reverse: a singleton should not capture values that belong to one request, user, tenant, or job.

Keep Request Values Scoped

// GOOD: shared behavior/resource, no request values captured.
@Component({ scope: LoadAs.Singleton })
export class ReportCache {
  private readonly entries = new Map<string, string>();

  get(key: string): string | undefined {
    return this.entries.get(key);
  }

  set(key: string, value: string): void {
    this.entries.set(key, value);
  }
}

// GOOD: request-specific values stay scoped.
@Component({ scope: LoadAs.Scoped })
export class ReportContext {
  constructor(@Inject(USER_ID) private userId: string) {}
}

// BAD: this singleton captures the first resolved userId forever.
@Component({ scope: LoadAs.Singleton })
export class BadReportContext {
  constructor(@Inject(USER_ID) private userId: string) {}
}

Rule of Thumb

If a value can change between two calls to the same application process, do not store it in a singleton constructor field. Pass it into a method or resolve a scoped collaborator for that call.

Use the Singleton page for the full lifetime reference. This page focuses on what changes during nested resolution: scoped/request values move through the graph for one resolution, while singleton collaborators remain shared.

NoEgo

© 2025 NoEgo. All rights reserved.