Skip to content

Solving NestJS Circular Dependencies in Microservices

A comprehensive guide to fixing the dreaded β€œCannot read properties of undefined” error when using forwardRef in NestJS microservices with TCP transport.


When calling a service via Microservice TCP transport that uses forwardRef, you’ll encounter:

Terminal window
TypeError: Cannot read properties of undefined (reading 'find')

The this.typeOrmRepository or other dependencies become undefined in the method being called.

Call MethodResult
Direct HTTP API call (e.g., GET /data-owner-bc/v1/resources)βœ… Works perfectly
Via Microservice TCP (e.g., consumer β†’ data-owner)❌ Repository/Services are undefined
Terminal window
Controller visitRepository ---> Repository { ... } # βœ… Controller has repo
data ---> { ... }
repo visitRepo ---> undefined # ❌ Service has NO repo
DEBUG CONSTRUCTOR Repo: Repository { ... } # πŸ”΄ Constructor called AFTER method!

The problem occurs when using forwardRef() in service constructors called via Microservice transport.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ VisitModule │◄───────►│ PatientModule β”‚
β”‚ β”‚forwardRefβ”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ VisitsService │────────►│ PatientsService β”‚
β”‚ β”‚forwardRefβ”‚ β”‚
β”‚ + 4 more β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ forwardRef β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
πŸ’₯ Microservice message arrives
πŸ’₯ Everything is undefined!
@Injectable()
export class VisitsService extends BaseServiceOperations<Visit, CreateVisitDTO, UpdateVisitDTO> {
constructor(
@InjectRepository(Visit, AppDatabases.APP_CORE)
visitRepository: Repository<Visit>,
// ⚠️ forwardRef causes undefined in Microservice context
@Inject(forwardRef(() => VisitOrdersService))
private readonly visitOrdersService: VisitOrdersService,
@Inject(forwardRef(() => PatientInsurancesService))
private readonly patientInsurancesService: PatientInsurancesService,
@Inject(forwardRef(() => PatientsService))
private readonly patientsService: PatientsService,
@Inject(forwardRef(() => AssessmentsService))
private readonly assessmentsService: AssessmentsService,
) {
super(visitRepository, {...});
}
}
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ βœ… HTTP Context (Normal Behavior) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ [App Bootstrap] ──────────────────────────────────────────────────────► β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β–Ί Resolve all providers β”‚
β”‚ β”œβ”€β–Ί Resolve forwardRefs β”‚
β”‚ └─► app.listen() ← Everything ready 100% β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ [HTTP Request arrives] β”‚
β”‚ β”‚ β”‚
β”‚ └─► Service ready 100% βœ… β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ❌ Microservice TCP Context (Problematic) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ [App Bootstrap] ──────────────────────────────────────────────────────► β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β–Ί Resolve providers (in progress...) β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ [TCP Message arrives!] ← Doesn't wait for bootstrap β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β–Ό β”‚
β”‚ β”‚ β”‚ [NestJS needs Service] β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”œβ”€β–Ί Instance exists (partially initialized) β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ └─► Uses that instance! ⚠️ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β–Ό β”‚
β”‚ β”‚ β”‚ [Method called] β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ this.repo = undefined ❌ β”‚
β”‚ β”‚ β”‚ this.service = undefined ❌ β”‚
β”‚ β”‚ β”‚ (constructor not finished!) β”‚
β”‚ β”‚ β”‚
β”‚ β”œβ”€β–Ί Still resolving forwardRefs... β”‚
β”‚ └─► (Too late, method already called) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Instead of Services talking directly to each other (requiring forwardRef), create an Orchestrator Service to coordinate complex workflows.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ❌ Before (Problematic) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” forwardRef β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚VisitsService │◄─────────────►│PatientsServiceβ”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ + forwardRef β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ - VisitOrders β”‚
β”‚ β”‚ - Assessments πŸ’₯ Circular Dependency β”‚
β”‚ β”‚ - VitalSigns πŸ’₯ Repository = undefined β”‚
β”‚ β”‚ - Patients πŸ’₯ Microservice unusable β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ βœ… After (Facade Pattern) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ VisitOperationsService β”‚ β”‚
β”‚ β”‚ (Facade / Orchestrator) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β€’ Complex workflows β”‚ β”‚
β”‚ β”‚ β€’ Transaction management β”‚ β”‚
β”‚ β”‚ β€’ Cross-service coordination β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”‚ inject (No forwardRef needed!) β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚VisitsService β”‚ β”‚ Assessments β”‚ β”‚ Patients β”‚ β”‚
β”‚ β”‚(Pure CRUD) β”‚ β”‚ Service β”‚ β”‚ Service β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ (Pure CRUD) β”‚ β”‚ (Pure CRUD) β”‚ β”‚
β”‚ β”‚No forwardRef β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ βœ… No Circular Dependency β”‚
β”‚ βœ… One-way dependencies only β”‚
β”‚ βœ… Microservice works 100% β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
PrincipleDescription
Separation of ConcernsEach Service manages CRUD for its own Entity only
One-way DependenciesDependencies flow from Facade down to Services only
No forwardRefAbsolutely no forwardRef anywhere
Single OrchestratorAll complex workflows live in one Facade
/**
* Facade/Orchestrator Service for managing complex workflows.
*
* Acts as a "manager" that orchestrates multiple Services
* without creating Circular Dependencies.
*/
@Injectable()
export class VisitOperationsService {
constructor(
private readonly dataSource: DataSource,
// Inject all services β€” NO forwardRef needed!
private readonly visitsService: VisitsService,
private readonly assessmentsService: AssessmentsService,
private readonly vitalSignsService: VitalSignsService,
private readonly visitOrdersService: VisitOrdersService,
private readonly visitRightsService: VisitRightsService,
private readonly groupOrdersService: GroupOrdersService,
private readonly patientsService: PatientsService,
private readonly patientInsurancesService: PatientInsurancesService,
) {}
/**
* Complex workflow requiring multiple services.
* Uses a Transaction for data consistency.
*/
async upsertAssessmentExamination(
data: CreateAssessmentExaminationDTO,
currentUser: IUserSession,
): Promise<Assessment> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const visit = await this.getOrCreateVisit(queryRunner, data, currentUser);
const assessment = await this.upsertAssessment(queryRunner, visit, data, currentUser);
await this.createVitalSigns(queryRunner, assessment, visit, data, currentUser);
await queryRunner.commitTransaction();
return assessment;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
// ... private helper methods
}
/**
* VisitsService β€” Pure CRUD Service for Visit Entity.
*
* Handles CRUD operations for Visit entity only.
* Complex workflows use VisitOperationsService instead.
*
* NO forwardRef anywhere!
*/
@Injectable()
export class VisitsService extends BaseServiceOperations<Visit, CreateVisitDTO, UpdateVisitDTO> {
constructor(
logger: LogsService,
configService: ConfigService,
@InjectRepository(Visit, AppDatabases.APP_CORE)
visitRepository: Repository<Visit>,
// External microservice clients only β€” NO forwardRef!
@Inject(AppMicroservice.SystemAdmin.name)
private readonly systemAdminService: ClientProxy,
) {
super(visitRepository, { ... });
}
// Pure CRUD + simple queries only
async findTodayVisit(resourceId: string, startOfDay: Date, endOfDay: Date): Promise<Visit | null> {
return this.repo.findOne({
where: {
resource_id: resourceId,
visit_date: Between(startOfDay, endOfDay),
is_deleted: false,
},
});
}
}
@Controller()
export class VisitEventsController {
constructor(
// Use Facade instead of VisitsService directly
private readonly visitOperationsService: VisitOperationsService,
) {}
@MessagePattern({ cmd: AppMicroservice.DataOwner.cmd.VisitEvents.UpsertAssessmentExamination })
async upsertAssessmentExamination(@Payload() payload: IMicroservicePayload<...>): Promise<Assessment> {
return this.visitOperationsService.upsertAssessmentExamination(
payload.payload.data,
payload.payload.current_user,
);
}
}
@Module({
imports: [
PatientModule, // No forwardRef needed anymore!
TypeOrmModule.forFeature([Visit, ...], AppDatabases.APP_CORE),
],
providers: [
// Pure CRUD Services β€” no dependencies between them
VisitsService,
AssessmentsService,
VitalSignsService,
VisitOrdersService,
GroupOrdersService,
VisitRightsService,
// Facade β€” injects all services
VisitOperationsService,
],
exports: [
VisitsService,
AssessmentsService,
VisitOperationsService,
],
})
export class VisitModule {}
βœ… Use Facade❌ Don’t Use Facade
Workflow requires multiple servicesSimple CRUD operations
Transactions across entitiesQueries within single entity
Complex business logicNo cross-service communication
Data consistency is critical

The Facade Pattern isn’t just about fixing circular dependencies β€” it serves as a critical Transactional Boundary.

Consider a real-world scenario: Onboarding a new resource. This single action touches multiple domains:

  1. Resource Domain: Update status to β€˜Active’, assign ID.
  2. Billing Domain: Open a new account.
  3. Records Domain: Create an initial record.
  4. Notification Domain: Alert the account manager.

Without a Facade, ResourceService would need to depend on BillingService, RecordsService, and NotificationService, creating a massive web of circular dependencies.

Solution with ResourceOnboardingFacade:

@Injectable()
export class ResourceOnboardingFacade {
constructor(
private readonly dataSource: DataSource,
private readonly resourceService: ResourceService,
private readonly billingService: BillingService,
private readonly recordsService: RecordsService,
private readonly notificationService: NotificationService,
) {}
async onboardResource(resourceId: string, roomNumber: string, user: IUserSession) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Update Resource (pass QueryRunner for transaction)
const resource = await this.resourceService.activate(
queryRunner,
resourceId,
'ACTIVE',
);
// 2. Open Billing Account
await this.billingService.openAccount(queryRunner, resourceId, user.id);
// 3. Create Initial Record
await this.recordsService.createOnboardingRecord(queryRunner, resource, user.id);
// Commit only if all above succeed
await queryRunner.commitTransaction();
// 4. Notifications (outside transaction β€” event-based)
await this.notificationService.notifyAccountManager(resource);
return resource;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}

This keeps ResourceService completely ignorant of BillingService, eliminating circular dependencies while ensuring data integrity.


Another powerful approach is using Event-Driven Architecture to reduce coupling between Services.

Best for communication within the same application.

Terminal window
npm install @nestjs/event-emitter
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
EventEmitterModule.forRoot({
wildcard: true,
delimiter: '.',
maxListeners: 10,
verboseMemoryLeak: true,
}),
],
})
export class AppModule {}
events/visit-created.event.ts
export class VisitCreatedEvent {
constructor(
public readonly visitId: string,
public readonly resourceId: string,
public readonly createdBy: string,
public readonly createdAt: Date,
) {}
}
// events/assessment-requested.event.ts
export class AssessmentRequestedEvent {
constructor(
public readonly visitId: string,
public readonly resourceId: string,
public readonly assessmentData: {
chief_complaint: string;
urgency_code: string;
vital_signs: any[];
},
public readonly requestedBy: string,
) {}
}
visits.service.ts
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class VisitsService {
constructor(
private readonly eventEmitter: EventEmitter2,
@InjectRepository(Visit, AppDatabases.APP_CORE)
private readonly visitRepo: Repository<Visit>,
// NO forwardRef!
) {}
async createVisit(data: CreateVisitDTO, currentUser: IUserSession): Promise<Visit> {
const visit = this.visitRepo.create(data);
const savedVisit = await this.visitRepo.save(visit);
// βœ… Emit event instead of calling other services directly
this.eventEmitter.emit(
'visit.created',
new VisitCreatedEvent(
savedVisit.id,
savedVisit.resource_id,
currentUser.id,
savedVisit.created_at,
),
);
return savedVisit;
}
async requestAssessment(
visitId: string,
assessmentData: any,
currentUser: IUserSession,
): Promise<void> {
// βœ… Emit event for AssessmentsService to handle
this.eventEmitter.emit(
'assessment.requested',
new AssessmentRequestedEvent(
visitId,
assessmentData.resource_id,
{
chief_complaint: assessmentData.chief_complaint,
urgency_code: assessmentData.urgency_code,
vital_signs: assessmentData.vital_signs,
},
currentUser.id,
),
);
}
}
assessments.service.ts
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class AssessmentsService {
constructor(
@InjectRepository(Assessment, AppDatabases.APP_CORE)
private readonly assessmentRepo: Repository<Assessment>,
// NO forwardRef!
) {}
// βœ… Listen to event and process when received
@OnEvent('visit.created')
async handleVisitCreated(event: VisitCreatedEvent): Promise<void> {
console.log(`Visit created: ${event.visitId} for resource: ${event.resourceId}`);
// No need to inject VisitsService at all!
}
@OnEvent('assessment.requested')
async handleAssessmentRequested(event: AssessmentRequestedEvent): Promise<void> {
const assessment = this.assessmentRepo.create({
visit_id: event.visitId,
resource_id: event.resourceId,
chief_complaint: event.assessmentData.chief_complaint,
urgency_code: event.assessmentData.urgency_code,
created_by: event.requestedBy,
});
await this.assessmentRepo.save(assessment);
}
// βœ… Using wildcard patterns
@OnEvent('visit.*')
async handleAllVisitEvents(event: any): Promise<void> {
console.log('Visit event received:', event);
}
}
@OnEvent('assessment.requested', { async: true })
async handleAssessmentRequestedAsync(event: AssessmentRequestedEvent): Promise<void> {
// This task runs async without blocking the main thread
await this.processLongRunningTask(event);
}

Best for communication between microservices in separate processes.

Terminal window
npm install @nestjs/bull bull
npm install @types/bull -D
app.module.ts
import { BullModule } from '@nestjs/bull';
@Module({
imports: [
BullModule.forRoot({
redis: {
host: 'localhost',
port: 6379,
},
}),
BullModule.registerQueue({
name: 'assessment-queue',
}),
],
})
export class AppModule {}
visits.service.ts
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bull';
@Injectable()
export class VisitsService {
constructor(
@InjectQueue('assessment-queue')
private readonly assessmentQueue: Queue,
) {}
async createVisitWithAssessment(data: CreateVisitDTO): Promise<Visit> {
const visit = await this.createVisit(data);
// βœ… Enqueue job instead of calling service directly
await this.assessmentQueue.add(
'create-assessment',
{
visitId: visit.id,
resourceId: visit.resource_id,
assessmentData: data.assessment,
},
{
attempts: 3,
backoff: 5000,
removeOnComplete: true,
},
);
return visit;
}
}
assessment.processor.ts
import { OnQueueFailed, Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
/**
* Assessment Queue Processor.
* Processes jobs from the 'assessment-queue' independently.
*/
@Processor('assessment-queue')
export class AssessmentProcessor {
constructor(
@InjectRepository(Assessment, AppDatabases.APP_CORE)
private readonly assessmentRepo: Repository<Assessment>,
// NO forwardRef needed!
) {}
@Process('create-assessment')
async handleCreateAssessment(job: Job): Promise<void> {
const { visitId, resourceId, assessmentData } = job.data;
const assessment = this.assessmentRepo.create({
visit_id: visitId,
resource_id: resourceId,
...assessmentData,
});
await this.assessmentRepo.save(assessment);
}
@OnQueueFailed()
async handleFailed(job: Job, error: Error): Promise<void> {
console.error(`Job ${job.id} failed:`, error.message);
}
}
FeatureFacade PatternEventEmitterMessage Queue (Bull)
ScopeIn-processIn-processCross-process
Transaction Supportβœ… Excellent support❌ Difficult to implement❌ Difficult to implement
PersistenceN/A (synchronous)❌ Lost on restartβœ… Persisted in Redis
Retry MechanismManual implementation❌ Manual implementationβœ… Built-in with backoff
ComplexityLowLowMedium
PerformanceFast (direct call)Very fast (in-memory)Slower (network + Redis)
Best Use CaseComplex workflows requiring transactionsSimple decoupling, fire-and-forgetDistributed systems, background jobs
Error HandlingTry-catch blocksCustom error listenersBuilt-in failed job handlers
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” emit β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ VisitsService │──────────────►│ Event Bus β”‚
β”‚ β”‚ β”‚ (EventEmitterβ”‚
β”‚ Creates Visit β”‚ β”‚ or Queue) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ β”‚
β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Assessments β”‚ β”‚ Notifications β”‚ β”‚ Audit Logs β”‚
β”‚ Service β”‚ β”‚ Service β”‚ β”‚ Service β”‚
β”‚ @OnEvent() β”‚ β”‚ @OnEvent() β”‚ β”‚ @OnEvent() β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Benefits:

  • No circular dependencies β€” Services don’t know about each other
  • Independent services β€” Each service operates autonomously
  • Easy to extend β€” Add new listeners without modifying existing code
  • Loose coupling β€” Producer doesn’t care who consumes the event

When a Service in one Module needs to call a Service in another Module, creating a Circular Dependency:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ❌ Cross-Module Circular Dependency β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ VisitModule β”‚ forwardRef β”‚ PatientModule β”‚ β”‚
β”‚ β”‚ │◄────────────►│ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ VisitsService │──┼──────────────┼─►│PatientsServiceβ”‚ β”‚ β”‚
β”‚ β”‚ β”‚ Needs to call β”‚ β”‚ β”‚ β”‚ Needs to call β”‚ β”‚ β”‚
β”‚ β”‚ β”‚patientsServiceβ”‚ β”‚ β”‚ β”‚ visitsService β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ πŸ’₯ Requires forwardRef at BOTH Module AND Service level β”‚
β”‚ πŸ’₯ Microservice call arrives β†’ undefined immediately β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
visit.module.ts
@Module({
imports: [
forwardRef(() => PatientModule), // ❌ forwardRef at Module level
],
providers: [VisitsService],
exports: [VisitsService],
})
export class VisitModule {}
// visits.service.ts
@Injectable()
export class VisitsService {
constructor(
@Inject(forwardRef(() => PatientsService)) // ❌ forwardRef at Service level
private readonly patientsService: PatientsService,
) {}
async createVisit(data: CreateVisitDTO): Promise<Visit> {
const patient = await this.patientsService.findById(data.patient_id);
// ❌ In microservice context: this.patientsService is undefined!
}
}
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ βœ… Solution: Event-Driven Communication β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ VisitModule β”‚ β”‚ PatientModule β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ emit β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ VisitsService │──┼─────────────►│ β”‚PatientsServiceβ”‚ β”‚ β”‚
β”‚ β”‚ β”‚ emit: β”‚ β”‚ Event Bus β”‚ β”‚ @OnEvent: β”‚ β”‚ β”‚
β”‚ β”‚ β”‚patient.validateβ”‚ β”‚ β”‚ β”‚patient.validateβ”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ @OnEvent: │◄─┼──────────────┼──│ emit: β”‚ β”‚ β”‚
β”‚ β”‚ β”‚visit.requestedβ”‚ β”‚ β”‚ β”‚visit.requestedβ”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ βœ… No forwardRef required β”‚
β”‚ βœ… Modules are completely independent β”‚
β”‚ βœ… Microservice works 100% reliably β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
libs/common/events/resource.events.ts
export class ResourceValidateRequestEvent {
constructor(
public readonly resourceId: string,
public readonly requestId: string, // For correlating response
) {}
}
export class ResourceValidateResponseEvent {
constructor(
public readonly requestId: string,
public readonly isValid: boolean,
public readonly resource: Resource | null,
public readonly error?: string,
) {}
}
// libs/common/events/visit.events.ts
export class VisitsByResourceRequestEvent {
constructor(
public readonly resourceId: string,
public readonly requestId: string,
) {}
}
export class VisitsByResourceResponseEvent {
constructor(
public readonly requestId: string,
public readonly visits: Visit[],
) {}
}
libs/common/services/event-request.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { v4 as uuidv4 } from 'uuid';
/**
* Helper service for request-response pattern using EventEmitter.
* Simulates synchronous service calls using asynchronous events.
*/
@Injectable()
export class EventRequestService {
private readonly pendingRequests = new Map<
string,
{
resolve: (value: any) => void;
reject: (error: any) => void;
timeout: NodeJS.Timeout;
}
>();
constructor(private readonly eventEmitter: EventEmitter2) {}
/**
* Emit an event and wait for response.
* Implements request-response pattern via EventEmitter.
*/
async emitAndWait<TRequest, TResponse>(
requestEvent: string,
responseEvent: string,
payload: TRequest,
timeoutMs: number = 5000,
): Promise<TResponse> {
const requestId = uuidv4();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pendingRequests.delete(requestId);
reject(new Error(`Event request timeout: ${requestEvent}`));
}, timeoutMs);
this.pendingRequests.set(requestId, { resolve, reject, timeout });
const handler = (response: any) => {
if (response.requestId === requestId) {
clearTimeout(timeout);
this.pendingRequests.delete(requestId);
this.eventEmitter.off(responseEvent, handler);
resolve(response);
}
};
this.eventEmitter.on(responseEvent, handler);
this.eventEmitter.emit(requestEvent, { ...payload, requestId });
});
}
}
@Injectable()
export class VisitsService {
constructor(
@InjectRepository(Visit, AppDatabases.APP_CORE)
private readonly visitRepo: Repository<Visit>,
private readonly eventEmitter: EventEmitter2,
private readonly eventRequest: EventRequestService,
// βœ… NO forwardRef(() => PatientsService) needed!
) {}
async createVisit(data: CreateVisitDTO, currentUser: IUserSession): Promise<Visit> {
// βœ… Use Event instead of calling resourcesService.findById() directly
const validateResponse = await this.eventRequest.emitAndWait<
ResourceValidateRequestEvent,
ResourceValidateResponseEvent
>('resource.validate.request', 'resource.validate.response', { resourceId: data.resource_id });
if (!validateResponse.isValid) {
throw new BadRequestException(
validateResponse.error || `Resource ${data.resource_id} not found`,
);
}
return this.visitRepo.save(
this.visitRepo.create({ ...data, created_by: currentUser.id }),
);
}
// βœ… Listen for "get visits by resource" requests from other modules
@OnEvent('visits.by-resource.request')
async handleVisitsByResourceRequest(event: VisitsByResourceRequestEvent): Promise<void> {
const visits = await this.visitRepo.find({
where: { resource_id: event.resourceId, is_deleted: false },
});
this.eventEmitter.emit(
'visits.by-resource.response',
new VisitsByResourceResponseEvent(event.requestId, visits),
);
}
}
@Injectable()
export class ResourcesService {
constructor(
@InjectRepository(Resource, AppDatabases.APP_CORE)
private readonly resourceRepo: Repository<Resource>,
private readonly eventEmitter: EventEmitter2,
private readonly eventRequest: EventRequestService,
// βœ… NO forwardRef(() => VisitsService) needed!
) {}
@OnEvent('resource.validate.request')
async handleResourceValidateRequest(event: ResourceValidateRequestEvent): Promise<void> {
try {
const resource = await this.resourceRepo.findOne({
where: { id: event.resourceId, is_deleted: false },
});
this.eventEmitter.emit(
'resource.validate.response',
new ResourceValidateResponseEvent(event.requestId, !!resource, resource),
);
} catch (error) {
this.eventEmitter.emit(
'resource.validate.response',
new ResourceValidateResponseEvent(event.requestId, false, null, error.message),
);
}
}
async getResourceWithVisits(resourceId: string) {
const resource = await this.resourceRepo.findOne({
where: { id: resourceId, is_deleted: false },
});
if (!resource) {
throw new NotFoundException(`Resource ${resourceId} not found`);
}
// βœ… Use Event instead of calling visitsService.findByResourceId() directly
const visitsResponse = await this.eventRequest.emitAndWait<
VisitsByResourceRequestEvent,
VisitsByResourceResponseEvent
>('visits.by-resource.request', 'visits.by-resource.response', { resourceId });
return { ...resource, visits: visitsResponse.visits };
}
}
visit.module.ts
@Module({
imports: [
CommonModule, // βœ… Provides EventEmitterModule and EventRequestService
TypeOrmModule.forFeature([Visit], AppDatabases.APP_CORE),
// βœ… NO forwardRef(() => ResourceModule) needed!
],
providers: [VisitsService],
exports: [VisitsService],
})
export class VisitModule {}
// resource.module.ts
@Module({
imports: [
CommonModule, // βœ… Provides EventEmitterModule and EventRequestService
TypeOrmModule.forFeature([Resource], AppDatabases.APP_CORE),
// βœ… NO forwardRef(() => VisitModule) needed!
],
providers: [ResourcesService],
exports: [ResourcesService],
})
export class ResourceModule {}

If you don’t need to wait for a response:

@Injectable()
export class VisitsService {
constructor(
@InjectRepository(Visit)
private readonly visitRepo: Repository<Visit>,
private readonly eventEmitter: EventEmitter2,
) {}
async createVisit(data: CreateVisitDTO): Promise<Visit> {
const visit = await this.visitRepo.save(this.visitRepo.create(data));
// βœ… Fire-and-forget: Notify other modules asynchronously
this.eventEmitter.emit('visit.created', {
visitId: visit.id,
resourceId: visit.resource_id,
});
return visit;
}
}
// resources.service.ts β€” listen for side effects
@Injectable()
export class ResourcesService {
@OnEvent('visit.created')
async handleVisitCreated(event: { visitId: string; resourceId: string }): Promise<void> {
// Update resource's last activity date
await this.resourceRepo.update(event.resourceId, {
last_activity_at: new Date(),
});
}
}
AspectforwardRefEventEmitter
Microservice Compatibility❌ Breaks immediatelyβœ… Works perfectly
CouplingHigh (tight coupling)Low (loose coupling)
TestingDifficult (must mock many services)Easy (mock EventEmitter only)
DebuggingHard (circular dependencies obscure flow)Easy (trace event flow)
Sync/AsyncSynchronousAsynchronous (requires handling)
Transaction SupportEasy (direct calls)Requires careful design

1. Golden Rule: Never Use forwardRef in Services Called via Microservice

Section titled β€œ1. Golden Rule: Never Use forwardRef in Services Called via Microservice”
// ❌ DON'T DO THIS β€” Will break when called via Microservice
@Injectable()
export class VisitsService {
constructor(
@Inject(forwardRef(() => PatientsService))
private readonly patientsService: PatientsService, // undefined in microservice context!
) {}
}
// βœ… DO THIS INSTEAD β€” No forwardRef, inject only what you need
@Injectable()
export class VisitsService {
constructor(
@InjectRepository(Visit, AppDatabases.APP_CORE)
private readonly visitRepo: Repository<Visit>,
// Use EventEmitter or Facade Pattern for cross-module communication
) {}
}
βœ… GOOD β€” Single Responsibility Principle:
β”œβ”€β”€ VisitsService (Pure CRUD for Visit)
β”œβ”€β”€ AssessmentsService (Pure CRUD for Assessment)
β”œβ”€β”€ ResourcesService (Pure CRUD for Resource)
β”œβ”€β”€ VisitOperationsService (Facade for complex workflows within module)
└── EventRequestService (Cross-module communication)
❌ BAD β€” God Service Anti-pattern:
└── VisitsService (Does everything + filled with forwardRef)
SituationSolution
Within Same Module + Complex workflow + Needs TransactionFacade Pattern
Cross-Module (different modules)EventEmitter
Fire-and-forget, don’t need to wait for resultEventEmitter (async)
Cross-service communication (different microservices)Message Queue
Background jobsMessage Queue
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Need to call another β”‚
β”‚ Service? β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Within the same Module? β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ YES β”‚ β”‚ NO
β–Ό β”‚ β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Complex workflow? β”‚ β”‚ β”‚ Use EventEmitter β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ YES β”‚ β”‚ NO β”‚
β–Ό β”‚ β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ Facade β”‚ β”‚ β”‚ Direct β”‚ β”‚
β”‚ Pattern β”‚ β”‚ β”‚ Inject β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ (No forwardRef β”‚
β”‚ if no circular) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

When you encounter Repository/Service undefined errors in Microservice context:

  • Check for forwardRef in Service constructors
  • If forwardRef within same Module β†’ Use Facade Pattern
  • If forwardRef across Modules β†’ Use EventEmitter
  • Remove forwardRef from both Service and Module level
  • Verify no circular imports between Modules
  • Ensure proper module imports in app.module.ts
  • Check TypeORM feature imports match correct database

1. Service has forwardRef in constructor
↓
2. Microservice TCP message arrives before dependencies fully resolve
↓
3. NestJS sends partially initialized instance
↓
4. All properties in instance = undefined
ScenarioSolutionExample
Within Module + TransactionFacade PatternVisitsService + AssessmentsService β†’ VisitOperationsService
Cross-ModuleEventEmitterVisitModule ↔ ResourceModule
Cross-MicroserviceMessage Queuedata-owner-bc ↔ data-consumer-bc
Fire-and-forgetEventEmitterAudit logs, Notifications