Microservice Communication Patterns
Common Patterns and Best Practices
Section titled “Common Patterns and Best Practices”1. Bounded Context Ownership
Section titled “1. Bounded Context Ownership”Rule: Each BC owns its domain data. Never access another BC’s database directly.
// ❌ BAD: Data Consumer BC directly querying Data Owner BC's databaseconst resource = await this.dataOwnerRepository.findOne(resourceId);
// ✅ GOOD: Data Consumer BC requesting data via the Data Owner BC microserviceclass 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:
- Extracts user session and trace headers from incoming request
- Wraps payload in
IMicroservicePayloadformat:{ payload: {...}, _context: {...} } - Logs outgoing request with correlation ID
- Sends message to target microservice
- Logs response with duration
- 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>whereTis your expected payload type - Always set logger context from
data._contextfor distributed tracing - Access actual data via
data.payload - Return plain objects (automatically serialized)
1.3 IMicroservicePayload Structure
Section titled “1.3 IMicroservicePayload Structure”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
1.4 Defining Message Patterns in Enum
Section titled “1.4 Defining Message Patterns in Enum”Rule: Always define message patterns in libs/common/src/enum/app-microservice.enum.ts instead of using string literals.
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,);1.5 Complete Communication Flow Example
Section titled “1.5 Complete Communication Flow Example”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:
- ✅ Use
MicroserviceClientServicefor all inter-service calls - ✅ Create
EventsControllerclasses for microservice endpoints - ✅ Use
IMicroservicePayload<T>for type-safe payloads - ✅ Define all commands in
app-microservice.enum.ts - ✅ Always propagate context for distributed tracing
- ✅ 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):
@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):
@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
2. Message Pattern Naming
Section titled “2. Message Pattern Naming”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 authorization3. Error Handling
Section titled “3. Error Handling”Always throw specific exceptions, never generic Error:
import { BadRequestException, NotFoundException } from '@nestjs/common';
// ❌ BADthrow new Error('Resource not found');
// ✅ GOODthrow new NotFoundException('Resource with ID 123 not found');4. Response Transformation
Section titled “4. Response Transformation”Use @ResourceType() decorator on controllers for automatic JSON:API formatting:
@Controller('resources')@ResourceType('resources') // Sets response resource typeexport class ResourcesController { // Responses automatically wrapped in JSON:API format}5. Authentication
Section titled “5. Authentication”Use @Public() decorator to bypass authentication:
@Get('health')@Public() // No JWT requiredgetHealthCheck() { return { status: 'ok', version: '1.0' };}Running the Service
Section titled “Running the Service”Development Mode
Section titled “Development Mode”# Start individual service with watch modenpm run start:dev:your-service
# Start all servicesnpm run start:dev:all
# Start with debuggingnpm run start:debug:your-serviceProduction Build
Section titled “Production Build”# Build servicenpm run build:your-service
# Build all servicesnpm run build:all
# Run production buildnode dist/apps/your-service/mainTroubleshooting
Section titled “Troubleshooting”Port Already in Use
Section titled “Port Already in Use”# Find process using portlsof -i :4003
# Kill processkill -9 <PID>Environment Variables Not Loading
Section titled “Environment Variables Not Loading”- Check
.envfile exists in project root - Verify variable names match exactly (case-sensitive)
- Check Joi validation schema in
config.module.ts - Restart service after changing
.env
Microservice Connection Failed
Section titled “Microservice Connection Failed”- Verify microservice port in
.env - Check service is registered in
common.module.ts - Ensure
microservicePortEnvis set in bootstrap options - Check console logs for “Microservice is listening on port X”
Swagger Docs Not Showing
Section titled “Swagger Docs Not Showing”- Verify
@nestjs/swaggerplugin innest-cli.json - Check bootstrap swagger configuration
- Ensure controllers use decorators:
@ApiTags(),@ApiOperation() - Verify docs gateway URL configuration
Quick Reference
Section titled “Quick Reference”File Locations
Section titled “File Locations”| Task | File Path |
|---|---|
| Bootstrap configuration | apps/<service>/src/main.ts |
| Environment validation | libs/config/src/config.module.ts |
| Environment variables | .env |
| Microservice enum | libs/common/src/enum/app-microservice.enum.ts |
| Client registration | libs/common/src/common.module.ts |
| Docs gateway | apps/docs-gateway/src/main.ts |
| CLI configuration | nest-cli.json |
Environment Variable Template
Section titled “Environment Variable Template”YOUR_PREFIX_NAME=your-serviceYOUR_PREFIX_VERSION=v1YOUR_BC_MODULE_MICROSERVICE_PORT=300XYOUR_BC_MODULE_HTTP_PORT=400X