ReferenceClass Runtime

DI Container & Scopes

Instance lifecycle scopes and the container API — register, resolve, and manage dependencies.

Continued from Dependency Injection — scopes, the container API, and operational notes.


Scopes

Three lifecycle scopes control how the container manages instances:

One instance shared across all resolve() calls:

@Service()
class ConfigService {
  readonly startTime = Date.now();
}

const a = container.resolve(ConfigService);
const b = container.resolve(ConfigService);
console.log(a === b); // true

New instance for each resolve() call:

@Service({ scope: 'transient' })
class RequestContext {
  readonly id = Math.random();
}

const a = container.resolve(RequestContext);
const b = container.resolve(RequestContext);
console.log(a === b); // false

One instance per request, shared within that request. Backed by tsyringe's ContainerScoped lifecycle and a per-request child container (createChild()):

@Service({ scope: 'request' })
class RequestId {
  readonly id = crypto.randomUUID();
}

const child = container.createChild(); // one child per request
const a = child.resolve(RequestId);
const b = child.resolve(RequestId);
console.log(a === b); // true — shared within the request

Request scope through controllers

With registerControllers, request scope is automatic. A controller (or any service in its dependency graph) that uses scope: 'request' is resolved from a fresh per-request child on each request; a purely-singleton controller keeps the memoized fast path with zero added per-request cost. See the class-based guide.


Container API

The global container and containers created with createContainer() implement Container.

container.register(token, provider)

Register a dependency using one of three provider types:

// Class provider
container.register(UserService, { useClass: UserServiceImpl });

// Value provider
container.register('CONFIG', { useValue: { port: 8080 } });

// Factory provider — receives the container for nested resolution
container.register('DB', {
  useFactory: (c) => createConnection(c.resolve<Config>('CONFIG')),
});

Provider Types

PropertyTypeDescription
useClassConstructor<T>Instantiate this class when the token is resolved
useValueTReturn this exact value when the token is resolved
useFactory(container: Container) => TCall this factory function when the token is resolved

container.resolve(token)

Resolve a dependency. Throws DependencyResolutionError if the token is not registered and cannot be auto-resolved.

const service = container.resolve(UserService);
const config = container.resolve<Config>('CONFIG');

container.resolveAll(token)

Resolve all instances registered under a token. Returns an empty array if none are registered.

container.register('Handler', { useValue: handlerA });
container.register('Handler', { useValue: handlerB });

const handlers = container.resolveAll<Handler>('Handler'); // [handlerA, handlerB]

container.isRegistered(token)

Check whether a token has been registered.

container.isRegistered(UserService); // true or false

container.clearInstances()

Clear cached singleton instances. Registrations remain — the next resolve() creates fresh instances.

beforeEach(() => {
  container.clearInstances();
});

container.reset()

Reset the container completely, removing all registrations and instances.

container.createChild()

Create a child container. The child inherits parent registrations but can override them independently.

const child = container.createChild();
child.register('CONFIG', { useValue: { port: 4000 } });
// Parent container still has original CONFIG

createContainer()

Create a new isolated container with no inherited registrations. Internally creates a child of the global tsyringe container and immediately resets it.

import { createContainer } from 'nextrush/class';

const testContainer = createContainer();
testContainer.register(UserRepository, { useClass: MockUserRepository });

Performance Notes

  • Singleton resolution pays the construction cost once. Subsequent calls return the cached instance with negligible overhead.
  • Transient resolution constructs a new instance on every call. Avoid transient scope for services resolved in hot paths (e.g., per-request middleware).
  • The delay() wrapper adds one level of indirection per resolution. Use it only for genuine circular dependencies.

Security Considerations

  • The container holds application-wide state. Do not expose it to untrusted code or serialize it.
  • Avoid storing secrets (API keys, credentials) as plain string tokens in client-accessible bundles. Use environment variables and resolve configuration at startup.
  • Factory providers receive the container instance. Ensure factory functions do not leak the container reference outside their scope.

When Not To Use

  • Pure functional applications with no class-based services. Manual wiring or factory functions are sufficient.
  • Very small applications (under 3–4 services) where the overhead of decorators and metadata outweighs the wiring pain.
  • Serverless functions that create a fresh process per invocation — singleton caching provides no benefit.

Next Steps

Was this helpful?

On this page