Microservices Questions
NestJS ships a first-class microservices layer that decouples your domain logic from the underlying transport. Interviewers use this topic to check whether you understand the difference between request/response and fire-and-forget semantics, how to pick a transporter, and how to keep a distributed system resilient. The questions below mirror what comes up in senior backend and platform interviews.
What is a transporter and which ones does Nest support?
A transporter is the strategy that moves messages between Nest microservices. The same @MessagePattern / @EventPattern handlers run regardless of transport — you only swap the transporter in the bootstrap config. This is the core selling point: business code is transport-agnostic.
| Transporter | Style | Built-in retries | Typical use |
|---|---|---|---|
| TCP | Request/response | No | Simple internal RPC |
| Redis | Pub/Sub | No | Lightweight events |
| NATS | Pub/Sub + req/res | Via NATS | High-throughput messaging |
| RabbitMQ | Queue (AMQP) | Yes (ack/nack) | Reliable work queues |
| Kafka | Log/stream | Yes (offsets) | Event streaming, replay |
| gRPC | Request/response | No (HTTP/2) | Typed contracts, polyglot |
// main.ts — a standalone microservice
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.KAFKA,
options: {
client: { brokers: ['localhost:9092'] },
consumer: { groupId: 'orders-consumer' },
},
},
);
await app.listen();
}
bootstrap();
What is the difference between @MessagePattern and @EventPattern?
This is the single most common question. @MessagePattern is request/response: the caller awaits a reply and the handler’s return value is sent back. @EventPattern is fire-and-forget: the producer publishes and does not wait — the handler returns nothing meaningful to the caller.
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
@Controller()
export class OrdersController {
// Request/response: client expects a value back
@MessagePattern({ cmd: 'sum' })
accumulate(@Payload() data: number[]): number {
return data.reduce((a, b) => a + b, 0);
}
// Fire-and-forget: no response channel
@EventPattern('order_created')
handleOrderCreated(@Payload() order: { id: string }) {
console.log(`Provisioning for order ${order.id}`);
}
}
On the client side, send() returns an Observable you subscribe to or await, while emit() is used for events.
import { Injectable, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class GatewayService {
constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {}
total(nums: number[]): Promise<number> {
return firstValueFrom(this.client.send({ cmd: 'sum' }, nums));
}
notifyCreated(id: string) {
this.client.emit('order_created', { id }); // returns immediately
}
}
Output:
Provisioning for order 7f3c
Prefer events for cross-service side effects you do not need to wait on. Reserve
send()for queries where the caller genuinely needs the result — everysend()couples the caller’s latency to the callee’s.
What is a hybrid application?
A hybrid app runs an HTTP server and one or more microservice listeners in the same process. This is how an API gateway typically exposes REST/GraphQL externally while consuming a message broker internally.
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.connectMicroservice({
transport: Transport.RMQ,
options: { urls: ['amqp://localhost:5672'], queue: 'tasks_queue' },
});
await app.startAllMicroservices();
await app.listen(3000); // HTTP still listening
}
bootstrap();
How do you handle errors across microservices?
Exceptions thrown in a @MessagePattern handler are serialized and surface on the client as an RpcException. Use a dedicated RpcExceptionFilter rather than the HTTP filter, because there is no HTTP response to shape.
import { Catch, RpcExceptionFilter, ArgumentsHost } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { RpcException } from '@nestjs/microservices';
@Catch(RpcException)
export class GlobalRpcExceptionFilter implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, _host: ArgumentsHost): Observable<any> {
return throwError(() => exception.getError());
}
}
In a gateway, catch the RpcException and translate it into a proper HTTP status so external clients see meaningful errors.
How do retries and acknowledgements work?
Behavior depends on the transporter. With RabbitMQ and Kafka you can enable manual acknowledgement so a message is only removed once your handler succeeds — a thrown error leaves it for redelivery. For client-side resilience, RxJS operators like retry() and timeout() wrap the send() observable.
{
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'tasks_queue',
noAck: false, // manual ack
queueOptions: { durable: true },
},
}
import { firstValueFrom, retry, timeout } from 'rxjs';
firstValueFrom(
this.client.send({ cmd: 'sum' }, [1, 2, 3]).pipe(
timeout(2000),
retry({ count: 3, delay: 500 }),
),
);
Make handlers idempotent. At-least-once delivery (Kafka, RabbitMQ redelivery) means the same message can arrive twice; design so reprocessing is harmless, e.g. upserts keyed by an event ID.
Best Practices
- Keep handlers transport-agnostic; choose the transporter at bootstrap, not in business code.
- Use
@EventPattern/emit()for side effects and@MessagePattern/send()only for queries that need a reply. - Pick durable, ack-based transporters (RabbitMQ, Kafka) when message loss is unacceptable.
- Make every consumer idempotent to survive at-least-once redelivery.
- Register a dedicated
RpcExceptionFilterand map RPC errors to HTTP statuses at the gateway. - Add client-side
timeout()andretry()so one slow service does not stall the caller. - Define typed message contracts (DTOs, gRPC
.proto, or Avro schemas) shared across services.