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.