js

Build Complete Event-Driven Microservices Architecture with NestJS, RabbitMQ, and Redis

Learn to build scalable event-driven microservices with NestJS, RabbitMQ, and Redis. Master saga patterns, service discovery, and deployment strategies for production-ready systems.

Build Complete Event-Driven Microservices Architecture with NestJS, RabbitMQ, and Redis

As I worked on scaling a traditional monolithic application recently, I faced constant bottlenecks in synchronous communication between components. Every new feature caused cascading failures across the system. That frustration led me to explore event-driven microservices as a solution. Today, I’ll walk you through building a robust architecture using NestJS, RabbitMQ, and Redis that handles real e-commerce demands. Ready to transform how your services communicate?

Our architecture features six core services coordinated through events. When a user places an order, the Order Service publishes an OrderCreatedEvent. RabbitMQ routes this to the Inventory Service for stock checks. Upon success, a PaymentRequestedEvent triggers the Payment Service. Finally, the Notification Service confirms the order completion. This asynchronous flow prevents system-wide failures when one service struggles.

Let’s set up our monorepo foundation. I prefer a structured approach that keeps services independent yet manageable:

nest new ecommerce-microservices --package-manager npm
cd ecommerce-microservices
nest generate app api-gateway
nest generate app order-service
# Repeat for user, product, payment, notification services
nest generate library common

Install essential packages:

npm install @nestjs/microservices amqplib redis 
npm install @nestjs/config @nestjs/redis ioredis

Why do you think we separate services while sharing common libraries? It balances autonomy with consistency.

For cross-service communication, we establish a shared event contract. In our common library:

// base-event.interface.ts
export interface DomainEvent<T = any> {
  id: string;
  timestamp: Date;
  eventType: string;
  aggregateId: string;
  payload: T;
}

We then implement a base service class that handles event emission:

// base.service.ts
@Injectable()
export abstract class BaseService {
  protected events: DomainEvent[] = [];

  protected addEvent(event: DomainEvent): void {
    this.events.push(event);
  }

  protected async commitEvents(): Promise<void> {
    const events = [...this.events];
    this.events = [];
    for (const event of events) {
      await this.eventEmitter.emitAsync(event.eventType, event);
    }
  }
}

RabbitMQ becomes our nervous system. Notice how we configure durable queues to survive restarts:

// rabbitmq.config.ts
export const getRabbitMQConfig = (queue: string): RmqOptions => ({
  transport: Transport.RMQ,
  options: {
    urls: [process.env.RABBITMQ_URL],
    queue,
    queueOptions: {
      durable: true,
      arguments: { 'x-message-ttl': 60000 }
    },
    prefetchCount: 10,
    noAck: false
  }
});

In the Order Service, creating an order triggers our event chain:

// order.entity.ts
export class Order {
  constructor(public id: string, public userId: string, public items: OrderItem[]) {}

  create() {
    this.addEvent({
      eventType: 'OrderCreated',
      aggregateId: this.id,
      payload: this.serialize()
    });
  }
}

What happens if payment fails after inventory reservation? We implement sagas for transactional integrity. The Order Service orchestrates compensation actions:

// order.saga.ts
async handlePaymentFailedEvent(event: PaymentFailedEvent) {
  await this.inventoryService.reverseInventory(event.orderId);
  await this.notifyUser(`Payment failed for order ${event.orderId}`);
}

Redis solves two critical challenges: distributed caching and session management. In the Product Service:

// product.service.ts
async getProduct(id: string) {
  const cached = await this.redis.get(`product:${id}`);
  if (cached) return JSON.parse(cached);
  
  const product = await this.repository.findOne(id);
  await this.redis.set(`product:${id}`, JSON.stringify(product), 'EX', 60);
  return product;
}

For monitoring, we implement health checks that report to our API gateway:

// health.controller.ts
@Get('health')
@HealthCheck()
check() {
  return this.health.check([
    () => this.rabbitmq.pingCheck('rabbitmq'),
    () => this.redis.pingCheck('redis')
  ]);
}

Testing requires simulating our event bus. I use Jest mocks for unit tests:

// order.service.spec.ts
it('should publish OrderCreatedEvent', async () => {
  const emitSpy = jest.spyOn(eventEmitter, 'emitAsync');
  await orderService.createOrder(testOrder);
  expect(emitSpy).toHaveBeenCalledWith('OrderCreated', expect.any(Object));
});

Deployment becomes straightforward with Docker Compose:

# docker-compose.yml
services:
  rabbitmq:
    image: rabbitmq:3-management
  redis:
    image: redis:alpine
  order-service:
    build: ./apps/order-service
    depends_on:
      - rabbitmq
      - redis

Common pitfalls? I’ve learned to always set message TTLs in RabbitMQ to prevent dead-letter buildup. Also, validate Redis memory policies to avoid out-of-memory crashes during traffic spikes. Instrument everything with OpenTelemetry—you’ll thank me during 3 AM outages.

Building this transformed how I approach distributed systems. The decoupling allows teams to deploy independently while maintaining system integrity. Have you encountered scenarios where event-driven approaches solved your architectural headaches? Share your experiences below! If this guide helped you, please like and share—it helps others discover practical microservices patterns. What topics should I cover next?

Keywords: event-driven microservices, NestJS microservices architecture, RabbitMQ message queue, Redis distributed caching, microservices with Docker, saga pattern implementation, NestJS RabbitMQ integration, microservices health monitoring, distributed transaction patterns, scalable microservices deployment



Similar Posts
Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Apps in 2024

Learn to integrate Next.js with Prisma ORM for type-safe, full-stack web apps. Build powerful database-driven applications with seamless TypeScript support.

Blog Image
Build Production-Ready Event-Driven Architecture: Node.js, Redis Streams, TypeScript Guide

Learn to build scalable event-driven systems with Node.js, Redis Streams & TypeScript. Master event sourcing, error handling, and production deployment.

Blog Image
Build a High-Performance API Gateway with Fastify Redis and Rate Limiting in Node.js

Learn to build a production-ready API Gateway with Fastify, Redis rate limiting, service discovery & Docker deployment. Complete Node.js tutorial inside!

Blog Image
Complete Guide to Building Full-Stack Apps with Next.js and Prisma Integration in 2024

Learn to build powerful full-stack web apps by integrating Next.js with Prisma. Discover type-safe database operations, seamless API routes, and rapid development workflows for modern web projects.

Blog Image
Event Sourcing with EventStore and Node.js: Complete CQRS Architecture Implementation Guide

Master Event Sourcing with EventStore & Node.js. Learn CQRS architecture, aggregates, projections, and testing in this comprehensive TypeScript guide.