Skip to content

Microservice Communication Patterns

Rule: Each BC owns its domain data. Never access another BC’s database directly.

// ❌ BAD: Data Consumer BC directly querying Data Owner BC's database
const resource = await this.dataOwnerRepository.findOne(resourceId);
// ✅ GOOD: Data Consumer BC requesting data via the Data Owner BC microservice
class DataConsumerService {
constructor(
private readonly logger: LogsService,
private readonly microserviceClient: MicroserviceClientService,
@Inject(AppMicroservice.DataOwner.name)
private readonly dataOwnerService: ClientProxy,
) {}
async fetchResource(resourceId: string) {
return this.microserviceClient.sendWithContext<IResource>(
this.logger,
this.dataOwnerService,
{ cmd: AppMicroservice.DataOwner.cmd.GetResourceById },
{ id: resourceId },
);
}
}

1.1 Using MicroserviceClientService for Inter-Service Communication

Section titled “1.1 Using MicroserviceClientService for Inter-Service Communication”

Always use MicroserviceClientService instead of direct ClientProxy.send() calls. This service provides:

  • Automatic context propagation (user session, correlation ID, trace ID)
  • Structured logging (request/response with timing)
  • Standardized payload format (IMicroservicePayload)
  • Error handling and tracing

Service Example:

import { ClientProxy } from '@nestjs/microservices';
import { AppMicroservice } from '@lib/common/enum/app-microservice.enum';
import { LogsService } from '@lib/common/modules/log/logs.service';
import { MicroserviceClientService } from '@lib/common/services/microservice-client.service';
@Injectable({ scope: Scope.REQUEST })
export class ResourcesService {
constructor(
private readonly logger: LogsService,
private readonly microserviceClient: MicroserviceClientService,
@Inject(AppMicroservice.SystemAdmin.name)
private readonly usersService: ClientProxy,
@Inject(AppMicroservice.Storage.name)
private readonly storagesService: ClientProxy,
) {}
async findOne(id: string): Promise<Resource> {
const resource = await this.resourceRepository.findOne(id);
// ✅ GOOD: Call SystemAdmin BC to get user details
if (resource.created_by) {
resource.created_by = await this.microserviceClient.sendWithContext<Partial<IUser>>(
this.logger,
this.usersService,
{ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindCreatedBy },
{ id: resource.created_by?.toString() || '' },
null, // defaultValue if call fails
);
}
// ✅ GOOD: Call Storage service to get image URLs
const images = await this.microserviceClient.sendWithContext<ImagePayload[]>(
this.logger,
this.storagesService,
{ cmd: AppMicroservice.Storage.cmd.GetPath },
{ images: pathImagesArray },
null,
);
return resource;
}
}

What sendWithContext does automatically:

  1. Extracts user session and trace headers from incoming request
  2. Wraps payload in IMicroservicePayload format: { payload: {...}, _context: {...} }
  3. Logs outgoing request with correlation ID
  4. Sends message to target microservice
  5. Logs response with duration
  6. Handles errors and re-throws for global exception filter

1.2 Creating EventsController for Microservice Endpoints

Section titled “1.2 Creating EventsController for Microservice Endpoints”

Naming Convention: Create <ControllerName>EventsController to handle microservice communication (TCP transport), separate from HTTP controllers.

EventsController Example:

import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { AppMicroservice } from '@lib/common/enum/app-microservice.enum';
import { IMicroservicePayload } from '@lib/common/interfaces';
import { LogsService } from '@lib/common/modules/log/logs.service';
import { UsersService } from '../services/users.service';
@Controller()
export class UserEventsController {
constructor(
private readonly logger: LogsService,
private readonly usersService: UsersService,
private readonly configService: ConfigService,
) {}
// Handle microservice request: Find user by ID
@MessagePattern({ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindById })
async findById(@Payload() data: IMicroservicePayload<{ id: string }>) {
// Step 1: Set context from incoming payload for tracing
this.logger.setContextFromPayload(data._context);
this.logger.setContext(
this.configService.get('SYSTEM_ADMIN_PREFIX_NAME'),
this.configService.get('SYSTEM_ADMIN_PREFIX_VERSION'),
);
// Step 2: Log incoming request
this.logger.info({
message: `Received request to find user by ID: ${data.payload.id}`,
});
// Step 3: Execute business logic using payload data
const [user] = await this.usersService.findAll({
s: { id: data.payload.id },
});
// Step 4: Return result (automatically logged by caller)
return user;
}
@MessagePattern({ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindUsername })
async findUsername(@Payload() data: IMicroservicePayload<{ username: string }>) {
this.logger.setContextFromPayload(data._context);
this.logger.setContext(
this.configService.get('SYSTEM_ADMIN_PREFIX_NAME'),
this.configService.get('SYSTEM_ADMIN_PREFIX_VERSION'),
);
this.logger.info({
message: `Received request to find user by username: ${data.payload.username}`,
});
const [user] = await this.usersService.findAll({
s: { username: data.payload.username },
});
return user;
}
@MessagePattern({ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindCreatedBy })
async findCreatedBy(@Payload() data: IMicroservicePayload<{ id: string }>) {
const { id, username, profile_image_url } = await this.usersService.findById(
data.payload.id,
);
// Return only necessary fields — don't expose sensitive data
return { id, username, profile_image_url };
}
}

Key Points:

  • Use @MessagePattern() decorator with command from enum (not string literals)
  • Accept IMicroservicePayload<T> where T is your expected payload type
  • Always set logger context from data._context for distributed tracing
  • Access actual data via data.payload
  • Return plain objects (automatically serialized)

The IMicroservicePayload interface provides a standardized format for all inter-service communication:

export interface IMicroservicePayload<T> {
/**
* The actual business data for the request.
*/
payload: T;
/**
* Metadata context for tracing, logging, and authorization.
* The underscore prefix indicates that it is metadata.
*/
_context: ICallContext;
}
export interface ICallContext {
user: {
id?: string;
roles?: string[];
};
trace: {
correlation_id?: string; // Groups related requests across services
trace_id?: string; // Tracks request flow through system
};
}

Benefits:

  • Distributed Tracing: Correlation IDs link logs across microservices
  • User Context: Authorization checks without re-querying user data
  • Consistent Logging: All services log with same context structure
  • Debugging: Easy to trace requests through the entire system

Rule: Always define message patterns in libs/common/src/enum/app-microservice.enum.ts instead of using string literals.

libs/common/src/enum/app-microservice.enum.ts
const UserResources = {
FindUsername: 'systemAdminBC.userResources.findUsername',
FindById: 'systemAdminBC.userResources.findById',
GetUserAuthorization: 'systemAdminBC.getUserAuthorization',
FindCreatedBy: 'systemAdminBC.userResources.findCreatedBy',
};
export const SystemAdminMCS = {
name: 'SYSTEM_ADMIN_BC',
UserResources: { cmd: UserResources },
};
const StorageCommands = {
Upload: 'storage.uploadFile',
Remove: 'storage.deleteFile',
GetPath: 'storage.getPath',
};
export const StorageMCS = {
name: 'STORAGE_SERVICE',
cmd: StorageCommands,
};
export const AppMicroservice = {
Auth: AuthMCS,
Iam: IamMCS,
Storage: StorageMCS,
SystemAdmin: SystemAdminMCS,
// ... other services
};

Usage:

// ❌ BAD: Using string literals (hard to refactor, typo-prone)
@MessagePattern({ cmd: 'systemAdminBC.userResources.findById' })
async findById(@Payload() data: any) { ... }
this.client.send({ cmd: 'systemAdminBC.userResources.findById' }, { id: '123' });
// ✅ GOOD: Using enum (type-safe, refactorable, autocomplete)
@MessagePattern({ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindById })
async findById(@Payload() data: IMicroservicePayload<{ id: string }>) { ... }
this.microserviceClient.sendWithContext(
this.logger,
this.usersService,
{ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindById },
{ id: '123' },
null,
);

Scenario: Data Consumer BC needs user details from System Admin BC when fetching resource data.

Step 1: Define Command in Enum

const UserResources = {
FindCreatedBy: 'systemAdminBC.userResources.findCreatedBy',
};
export const SystemAdminMCS = {
name: 'SYSTEM_ADMIN_BC',
UserResources: { cmd: UserResources },
};

Step 2: Register Client in CommonModule

ClientsModule.registerAsync([
{
name: AppMicroservice.SystemAdmin.name,
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: 'localhost',
port: configService.get<number>('SYSTEM_ADMIN_BC_MODULE_MICROSERVICE_PORT', 3088),
},
}),
inject: [ConfigService],
},
]),

Step 3: Create EventsController in System Admin BC

@Controller()
export class UserEventsController {
@MessagePattern({ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindCreatedBy })
async findCreatedBy(@Payload() data: IMicroservicePayload<{ id: string }>) {
const { id, username, profile_image_url } = await this.usersService.findById(
data.payload.id,
);
return { id, username, profile_image_url };
}
}

Step 4: Call from Data Consumer BC Service

@Injectable({ scope: Scope.REQUEST })
export class ResourcesService {
constructor(
private readonly logger: LogsService,
private readonly microserviceClient: MicroserviceClientService,
@Inject(AppMicroservice.SystemAdmin.name)
private readonly usersService: ClientProxy,
) {}
async findOne(id: string): Promise<Resource> {
const resource = await this.resourceRepository.findOne(id);
if (resource.created_by) {
resource.created_by = await this.microserviceClient.sendWithContext<Partial<IUser>>(
this.logger,
this.usersService,
{ cmd: AppMicroservice.SystemAdmin.UserResources.cmd.FindCreatedBy },
{ id: resource.created_by?.toString() || '' },
null,
);
}
return resource;
}
}

Logs Generated (showing distributed tracing in action):

// Data Consumer BC logs (outgoing request)
{
"level": "info",
"message": "[Microservice Request] Sending command 'systemAdminBC.userResources.findCreatedBy'",
"context": {
"action": "MICROSERVICE_SEND_SYSTEMADMINBC.USERRESOURCES.FINDCREATEDBY",
"correlation_id": "abc-123-xyz",
"payload": { "id": "user-uuid-123" }
},
"service": "data-consumer-bc",
"version": "v1"
}
// System Admin BC logs (incoming request)
{
"level": "info",
"message": "Received request to find user by ID: user-uuid-123",
"context": { "correlation_id": "abc-123-xyz" },
"service": "system-admin-bc",
"version": "v1"
}
// Data Consumer BC logs (response received)
{
"level": "info",
"message": "[Microservice Response] Received response for command 'systemAdminBC.userResources.findCreatedBy'",
"context": {
"action": "MICROSERVICE_SUCCESS_SYSTEMADMINBC.USERRESOURCES.FINDCREATEDBY",
"correlation_id": "abc-123-xyz",
"duration_ms": 45
},
"service": "data-consumer-bc",
"version": "v1"
}

Key Takeaways:

  1. ✅ Use MicroserviceClientService for all inter-service calls
  2. ✅ Create EventsController classes for microservice endpoints
  3. ✅ Use IMicroservicePayload<T> for type-safe payloads
  4. ✅ Define all commands in app-microservice.enum.ts
  5. ✅ Always propagate context for distributed tracing
  6. ✅ Log incoming requests and set context in EventsControllers

1.6 Practical Example: Data Consumer BC Calling Data Owner BC

Section titled “1.6 Practical Example: Data Consumer BC Calling Data Owner BC”

Scenario: A consumer service needs resource data for an associated record. It must call the Data Owner BC microservice — never access the owner’s database directly.

Define Commands:

export const DataOwnerMCS = {
name: 'DATA_OWNER_BC_SERVICE',
cmd: { GetResourceById: 'dataOwner.resource.getById' },
};
export const AppMicroservice = {
// ... other services
DataOwner: DataOwnerMCS,
};

Data Owner BC EventsController (exposes resource data):

apps/data-owner-bc/src/modules/resource/controllers/resource-events.controller.ts
@Controller()
export class ResourceEventsController {
constructor(private readonly resourcesService: ResourcesService) {}
@MessagePattern({ cmd: AppMicroservice.DataOwner.cmd.GetResourceById })
async getResourceById(@Payload() data: IMicroservicePayload<{ id: string }>) {
return await this.resourcesService.findOne(data.payload.id);
}
}

Data Consumer BC Service (fetches resource data):

apps/data-consumer-bc/src/modules/records/services/records.service.ts
@Injectable({ scope: Scope.REQUEST })
export class RecordsService {
constructor(
private readonly logger: LogsService,
private readonly microserviceClient: MicroserviceClientService,
@Inject(AppMicroservice.DataOwner.name)
private readonly dataOwnerBcService: ClientProxy,
) {}
async getRecordWithResource(recordId: string) {
const record = await this.findById(recordId);
// ✅ Call Data Owner BC microservice (never access its database directly)
const resource = await this.microserviceClient.sendWithContext(
this.logger,
this.dataOwnerBcService,
{ cmd: AppMicroservice.DataOwner.cmd.GetResourceById },
{ id: record.resource_id },
null, // Return null if Data Owner fails
);
return { ...record, resource };
}
}

Request Flow: Client → Consumer Controller → Consumer Service → Data Owner BC microservice → Owner returns resource → Consumer combines data → Response


Format: {service}.{resource}.{action} or {service}.{action}

'dataOwner.resource.findById' // Get resource by ID
'reporting.report.generate' // Generate a report
'iam.user.authorize' // Check user authorization

Always throw specific exceptions, never generic Error:

import { BadRequestException, NotFoundException } from '@nestjs/common';
// ❌ BAD
throw new Error('Resource not found');
// ✅ GOOD
throw new NotFoundException('Resource with ID 123 not found');

Use @ResourceType() decorator on controllers for automatic JSON:API formatting:

@Controller('resources')
@ResourceType('resources') // Sets response resource type
export class ResourcesController {
// Responses automatically wrapped in JSON:API format
}

Use @Public() decorator to bypass authentication:

@Get('health')
@Public() // No JWT required
getHealthCheck() {
return { status: 'ok', version: '1.0' };
}

Terminal window
# Start individual service with watch mode
npm run start:dev:your-service
# Start all services
npm run start:dev:all
# Start with debugging
npm run start:debug:your-service
Terminal window
# Build service
npm run build:your-service
# Build all services
npm run build:all
# Run production build
node dist/apps/your-service/main

Terminal window
# Find process using port
lsof -i :4003
# Kill process
kill -9 <PID>
  1. Check .env file exists in project root
  2. Verify variable names match exactly (case-sensitive)
  3. Check Joi validation schema in config.module.ts
  4. Restart service after changing .env
  1. Verify microservice port in .env
  2. Check service is registered in common.module.ts
  3. Ensure microservicePortEnv is set in bootstrap options
  4. Check console logs for “Microservice is listening on port X”
  1. Verify @nestjs/swagger plugin in nest-cli.json
  2. Check bootstrap swagger configuration
  3. Ensure controllers use decorators: @ApiTags(), @ApiOperation()
  4. Verify docs gateway URL configuration

TaskFile Path
Bootstrap configurationapps/<service>/src/main.ts
Environment validationlibs/config/src/config.module.ts
Environment variables.env
Microservice enumlibs/common/src/enum/app-microservice.enum.ts
Client registrationlibs/common/src/common.module.ts
Docs gatewayapps/docs-gateway/src/main.ts
CLI configurationnest-cli.json
Terminal window
YOUR_PREFIX_NAME=your-service
YOUR_PREFIX_VERSION=v1
YOUR_BC_MODULE_MICROSERVICE_PORT=300X
YOUR_BC_MODULE_HTTP_PORT=400X