Skip to content
NestJS interview 4 min read

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.

TransporterStyleBuilt-in retriesTypical use
TCPRequest/responseNoSimple internal RPC
RedisPub/SubNoLightweight events
NATSPub/Sub + req/resVia NATSHigh-throughput messaging
RabbitMQQueue (AMQP)Yes (ack/nack)Reliable work queues
KafkaLog/streamYes (offsets)Event streaming, replay
gRPCRequest/responseNo (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 — every send() 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 RpcExceptionFilter and map RPC errors to HTTP statuses at the gateway.
  • Add client-side timeout() and retry() so one slow service does not stall the caller.
  • Define typed message contracts (DTOs, gRPC .proto, or Avro schemas) shared across services.
Last updated June 14, 2026
Was this helpful?