Circular dependencies
Sometimes two services need each other. Nael can resolve those cycles when you explicitly signal them with helpers like forwardRef
or by lazily fetching a provider from ModuleRef. This page covers the common escape hatches and when to reach for them.
Provider-level forwardRef
Wrap the token passed to @Inject() in forwardRef() so the container evaluates the reference only after both classes
are defined. This keeps constructor injection intact while avoiding undefined errors at runtime.
import { Inject, Injectable, forwardRef } from '@nl-framework/core';@Injectable()export class UsersService { constructor( @Inject(forwardRef(() => PaymentsService)) private readonly payments: PaymentsService, ) {} async createUser() { await this.payments.provisionWallet(); }}@Injectable()export class PaymentsService { constructor(private readonly users: UsersService) {} async provisionWallet() {/* ... */}}Consider whether the cycle reveals a tighter coupling than necessary. Sometimes extracting a shared interface or domain event eliminates the loop.
Module imports with forwardRef
Modules can also depend on each other. When both need to import the other’s exports, wrap the module reference in forwardRef
inside the imports array. Nael will inline a thunk that resolves to the actual module once it is defined.
import { Module, forwardRef } from '@nl-framework/core';@Module({ providers: [UsersService], exports: [UsersService], imports: [forwardRef(() => PaymentsModule)],})export class UsersModule {}@Module({ providers: [PaymentsService], exports: [PaymentsService], imports: [forwardRef(() => UsersModule)],})export class PaymentsModule {}Lazy resolution via ModuleRef
ModuleRef lets you resolve providers on demand, outside of the constructor. Use this when eager injection would create a cycle but
the dependency is only needed for specific code paths.
import { Injectable } from '@nl-framework/core';import { ModuleRef } from '@nl-framework/core';@Injectable()export class ReportsService { constructor(private readonly moduleRef: ModuleRef) {} async run() { const exporter = await this.moduleRef.resolve(ExporterService, { strict: false }); return exporter.export(); }}Passing {'{ strict: false }'} allows looking up tokens from imported modules. Prefer explicit module exports to keep ownership clear.
Setter or lifecycle injection
When you must reference a provider immediately after construction, you can inject a forward reference and assign it during lifecycle hooks
such as onModuleInit. It’s more verbose, but it keeps constructors minimal.
import { Injectable, Inject, forwardRef, OnModuleInit } from '@nl-framework/core';@Injectable()export class NotificationsService implements OnModuleInit { private emailService!: EmailService; constructor(@Inject(forwardRef(() => EmailService)) private readonly email: EmailService) {} onModuleInit() { this.emailService = this.email; }}@Injectable()export class EmailService { constructor(private readonly notifications: NotificationsService) {}}