mdashikjs/blog
All posts
Breaking a Monolith: NestJS Microservices with RabbitMQ
Backend

Breaking a Monolith: NestJS Microservices with RabbitMQ

Backend5 min

Breaking a Monolith: NestJS Microservices with RabbitMQ

Our monolith handled everything from auth to billing. When deploy times hit 12 minutes and one bad merge took down the whole system, we started extracting services. Here is how we did it without rewriting from scratch.

NestJSMicroservicesRabbitMQArchitecture
Share:

When to break the monolith

Not every monolith needs splitting. Ours needed it because three teams were stepping on each other — merge conflicts every other day, and the blast radius of any bug was the entire platform. If you have one team and manageable deploy times, keep the monolith.

Choosing the first service to extract

We picked the notification service. It had the clearest boundary: receive an event, format a message, send it. No shared state with the rest of the app, no complex queries. Low risk, high learning value.

RabbitMQ over HTTP

We considered REST calls between services but went with RabbitMQ. The reason: notifications are fire-and-forget. If the notification service is down for 30 seconds, we want messages to queue up, not fail. RabbitMQ gives us that durability for free.

// Producer (monolith)
@Injectable()
export class EventPublisher {
  constructor(@Inject('NOTIFICATIONS') private client: ClientProxy) {}

  async userRegistered(userId: string, email: string) {
    this.client.emit('user.registered', { userId, email });
  }
}
// Consumer (notification service)
@Controller()
export class NotificationController {
  @EventPattern('user.registered')
  async handleUserRegistered(data: { userId: string; email: string }) {
    await this.mailer.sendWelcome(data.email);
  }
}

Dead letter queues save you at 3am

Messages fail. The consumer crashes, the email provider is down, the payload is malformed. Without a dead letter queue, those messages vanish. We route every failed message to a DLQ and have a simple admin endpoint that lets us inspect and replay them.

The contract problem

The hardest part was not the infrastructure. It was keeping the message contracts in sync between producer and consumer. We created a shared types package published to our private npm registry. Both services import from it. If the shape changes, both sides see the TypeScript error at build time.

Results after 6 months

  • Deploy times: 12 min down to 3 min per service
  • Notification failures no longer crash the main app
  • Three teams deploy independently
  • RabbitMQ has handled 14M messages with zero data loss
MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.