Message and Event Patterns

Request-Response vs Fire-and-Forget

@MessagePattern + client.send()@EventPattern + client.emit()
StyleRequest-responseEvent, fire-and-forget
Caller waits?Yes, gets a replyNo
HandlersExactly oneZero 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.