Careeroza
Message and Event Patterns
Request-Response vs Fire-and-Forget
@MessagePattern + client.send() | @EventPattern + client.emit() | |
|---|---|---|
| Style | Request-response | Event, fire-and-forget |
| Caller waits? | Yes, gets a reply | No |
| Handlers | Exactly one | Zero or many |
| Example | "Reserve 2 units of product 7" -> ok / out of stock | "Order ORD-7781 was placed" |
apps/inventory/src/inventory.controller.ts
import { Controller } from '@nestjs/common';
import { EventPattern, MessagePattern, Payload, RpcException } from '@nestjs/microservices';
@Controller()
export class InventoryController {
constructor(private readonly inventory: InventoryService) {}
@MessagePattern({ cmd: 'reserve_stock' })
async reserve(@Payload() data: { productId: number; quantity: number }) {
const ok = await this.inventory.reserve(data.productId, data.quantity);
if (!ok) throw new RpcException({ code: 'OUT_OF_STOCK', productId: data.productId });
return { reserved: true };
}
@EventPattern('order_cancelled')
async release(@Payload() data: { orderId: string; items: { productId: number; quantity: number }[] }) {
await this.inventory.release(data.items);
}
}The Client Side (API Gateway)
apps/api/src/orders/orders.service.ts
import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { catchError, firstValueFrom, throwError, timeout } from 'rxjs';
@Injectable()
export class OrdersService {
constructor(@Inject('INVENTORY_SERVICE') private readonly inventory: ClientProxy) {}
async place(dto: CreateOrderDto) {
for (const item of dto.items) {
await firstValueFrom(
this.inventory.send({ cmd: 'reserve_stock' }, item).pipe(
timeout(3000),
catchError((err) => throwError(() => new UnprocessableEntityException(err))),
),
);
}
const order = await this.repo.create(dto);
this.inventory.emit('order_placed', { orderId: order.id, items: dto.items }); // no await needed
return order;
}
}
// orders.module.ts
// imports: [ClientsModule.register([{ name: 'INVENTORY_SERVICE', transport: Transport.TCP, options: { host: 'inventory', port: 4001 } }])]Both Patterns, Simulated
Example
A tiny in-process broker shows the difference: send goes to one handler and returns its reply, emit fans out to every subscriber and returns nothing.
type Handler = (data: any) => unknown;
class Broker {
private messageHandlers = new Map<string, Handler>();
private eventHandlers = new Map<string, Handler[]>();
messagePattern(pattern: string, h: Handler) { this.messageHandlers.set(pattern, h); }
eventPattern(pattern: string, h: Handler) { this.eventHandlers.set(pattern, [...(this.eventHandlers.get(pattern) ?? []), h]); }
async send(pattern: string, data: unknown) {
const h = this.messageHandlers.get(pattern);
if (!h) throw new Error('There is no matching message handler defined in the remote service.');
return h(data);
}
emit(pattern: string, data: unknown) {
for (const h of this.eventHandlers.get(pattern) ?? []) queueMicrotask(() => h(data));
}
}
const broker = new Broker();
const stock = new Map([[7, 3]]);
broker.messagePattern('reserve_stock', ({ productId, quantity }) => {
const left = stock.get(productId) ?? 0;
if (left < quantity) return { reserved: false, left };
stock.set(productId, left - quantity);
return { reserved: true, left: left - quantity };
});
broker.eventPattern('order_placed', (e) => console.log(' [notifications] SMS for', e.orderId));
broker.eventPattern('order_placed', (e) => console.log(' [analytics] recorded', e.orderId));
async function main() {
console.log('send ->', await broker.send('reserve_stock', { productId: 7, quantity: 2 }));
console.log('send ->', await broker.send('reserve_stock', { productId: 7, quantity: 2 }));
broker.emit('order_placed', { orderId: 'ORD-7781' });
console.log('emit returned immediately');
await new Promise((r) => setTimeout(r, 0));
try { await broker.send('refund', {}); } catch (e) { console.log('send ->', (e as Error).message); }
}
main();Output
send -> { reserved: true, left: 1 }
send -> { reserved: false, left: 1 }
emit returned immediately
[notifications] SMS for ORD-7781
[analytics] recorded ORD-7781
send -> There is no matching message handler defined in the remote service.