How I approach NestJS microservices: service boundaries first, transport second, and operational behavior always visible.
From my notebook
Where this came from
Most of my production broker work has used RabbitMQ. I wrote this note while comparing those lessons with a smaller public NestJS/Kafka configuration project. The transport changes, but ownership, idempotency, retries, and observability remain the hard parts.
My short checklist
Kafka, RabbitMQ, HTTP, and gRPC are implementation details compared with the harder question: where should the boundary exist? A microservice boundary should match ownership, data responsibility, deployment risk, or a clear scaling need. If the boundary only mirrors a folder name, it adds network complexity without buying independence.
Before introducing a message broker, I define what the service owns. Does it own the database table? Does it own the business rule? Can another team change it safely? What happens if it is down? These questions expose whether the service is a real boundary or just a remote function call.
A Kafka topic or queue message is a public API. Consumers may deploy at different times, retry old messages, or receive events produced by a previous version. Message names, required fields, optional fields, and versioning strategy matter. I prefer explicit event types and payloads that describe facts, not commands pretending to be facts.
NestJS makes it easy to build clean modules, but module boundaries are only useful if dependencies stay directional. A billing module importing support internals is a design smell. In a microservice setup, I keep transport adapters thin and put business rules in services that can be tested without a broker.
Distributed systems fail in ordinary ways: duplicate messages, late messages, missing dependencies, partial writes, schema drift, and poison payloads. A scalable system needs retry limits, dead-letter handling, backoff, health checks, and dashboards before it needs clever abstractions.
The best microservice work is usually conservative. Split only where independence is real, keep contracts small, make consumers idempotent, and make operational state visible. NestJS gives a productive structure, but the architecture comes from the boundaries and failure behavior, not from the decorator syntax.
@MessagePattern("invoice.created")
async handleInvoiceCreated(event: InvoiceCreatedEvent) {
const exists = await this.inbox.seen(event.id);
if (exists) return;
await this.billing.applyInvoice(event);
await this.inbox.markSeen(event.id);
}