js

Building Event-Driven Microservices: Complete NestJS, RabbitMQ, and MongoDB Production Guide

Learn to build scalable event-driven microservices with NestJS, RabbitMQ & MongoDB. Master CQRS, event sourcing & distributed systems. Complete tutorial.

Building Event-Driven Microservices: Complete NestJS, RabbitMQ, and MongoDB Production Guide

I’ve been thinking a lot about how modern applications need to handle complexity at scale. That’s what led me to explore event-driven microservices – a pattern that can transform how we build resilient, scalable systems. Today, I want to share my approach using NestJS, RabbitMQ, and MongoDB.

Setting up the foundation is straightforward with Docker. Here’s how I configure my development environment:

# docker-compose.yml
services:
  rabbitmq:
    image: rabbitmq:3-management
    ports: ["5672:5672", "15672:15672"]
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin

  mongodb:
    image: mongo:6
    ports: ["27017:27017"]
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: admin

Have you ever wondered how services stay decoupled while still communicating effectively? Event-driven architecture provides the answer. Let me show you how I define events that services can publish and consume:

// shared/events/order.events.ts
export class OrderCreatedEvent {
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly items: OrderItem[],
    public readonly total: number
  ) {}
}

The Order Service becomes the heart of our system. Notice how I use CQRS patterns to separate commands from queries:

// order.module.ts
@Module({
  imports: [
    MongooseModule.forFeature([{ name: Order.name, schema: OrderSchema }]),
    ClientsModule.register([{
      name: 'RABBITMQ_SERVICE',
      transport: Transport.RMQ,
      options: {
        urls: ['amqp://admin:admin@localhost:5672'],
        queue: 'orders_queue',
        queueOptions: { durable: true }
      }
    }]),
    CqrsModule
  ],
  controllers: [OrderController],
  providers: [OrderService, OrderCreatedHandler, CreateOrderHandler]
})
export class OrderModule {}

What happens when an order is created? Multiple services need to react without being tightly coupled. Here’s how I handle command execution and event publishing:

@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
  constructor(
    @InjectModel(Order.name) private orderModel: Model<OrderDocument>,
    private eventBus: EventBus
  ) {}

  async execute(command: CreateOrderCommand): Promise<Order> {
    const order = new this.orderModel({
      orderId: uuidv4(),
      userId: command.userId,
      items: command.items,
      total: command.items.reduce((sum, item) => 
        sum + (item.price * item.quantity), 0)
    });

    await order.save();
    this.eventBus.publish(new OrderCreatedEvent(
      order.orderId,
      order.userId,
      order.items,
      order.total
    ));
    
    return order;
  }
}

RabbitMQ acts as our message broker, ensuring reliable delivery between services. The Inventory Service listens for order events and updates stock levels accordingly:

// inventory.controller.ts
@Controller()
export class InventoryController {
  constructor(private readonly inventoryService: InventoryService) {}

  @EventPattern('order_created')
  async handleOrderCreated(data: OrderCreatedEvent) {
    await this.inventoryService.updateInventory(data.items);
  }
}

But what about data consistency across services? I implement compensating transactions for rollback scenarios. If inventory update fails, the order service needs to know:

// order.controller.ts
@MessagePattern('inventory_update_failed')
async handleInventoryFailure(data: { orderId: string; reason: string }) {
  await this.orderService.cancelOrder(data.orderId, data.reason);
  this.eventBus.publish(new OrderCancelledEvent(
    data.orderId,
    'Inventory insufficient'
  ));
}

Monitoring becomes crucial in distributed systems. I add structured logging and metrics collection:

// logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    logger.info(`Incoming request: ${request.method} ${request.url}`);
    
    return next.handle().pipe(
      tap(() => logger.info('Request completed successfully'))
    );
  }
}

Testing event-driven systems requires special attention. I use Docker Testcontainers for integration tests:

// order.e2e-spec.ts
describe('Order Service (e2e)', () => {
  let app: INestApplication;
  let rabbitmqContainer: StartedTestContainer;

  beforeAll(async () => {
    rabbitmqContainer = await new GenericContainer('rabbitmq:3-management')
      .withExposedPorts(5672)
      .start();
  });

  afterAll(async () => {
    await app.close();
    await rabbitmqContainer.stop();
  });
});

Deployment considerations include health checks and graceful shutdown:

// main.ts
async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    OrderModule,
    {
      transport: Transport.RMQ,
      options: {
        urls: [process.env.RABBITMQ_URL],
        queue: 'orders_queue',
        queueOptions: { durable: true },
        noAck: false,
        prefetchCount: 1
      }
    }
  );

  app.enableShutdownHooks();
  await app.listen();
}

Building event-driven microservices has transformed how I approach distributed systems. The combination of NestJS’s structure, RabbitMQ’s reliability, and MongoDB’s flexibility creates a powerful foundation. But remember – every system has unique requirements. What challenges have you faced with microservices?

I’d love to hear your thoughts and experiences. If this approach resonates with you, please share it with others who might benefit. Your comments and feedback help all of us learn and grow together.

Keywords: event-driven microservices, NestJS microservices, RabbitMQ integration, MongoDB microservices, CQRS pattern implementation, event sourcing tutorial, microservices architecture guide, distributed systems design, message queue programming, scalable backend development



Similar Posts
Blog Image
How Storybook Transformed My Angular Workflow and Simplified UI Development

Discover how integrating Storybook with Angular helps isolate components, improve testing, and streamline your UI development process.

Blog Image
How to Integrate Socket.IO with Next.js: Complete Guide for Real-Time Web Applications

Learn to integrate Socket.IO with Next.js for real-time features like live chat, notifications, and collaborative editing. Build modern web apps with seamless real-time communication today.

Blog Image
Complete Guide to Next.js and Prisma ORM Integration: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven apps. Build full-stack applications with seamless data flows and improved developer experience.

Blog Image
Complete Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Applications with Modern ORM

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack applications. Complete guide with setup, queries, and best practices.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM: Build Type-Safe Full-Stack Applications

Learn how to integrate Next.js with Prisma ORM for type-safe, database-driven web applications. Build faster with seamless TypeScript support and modern development tools.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Operations

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable web apps. Discover seamless database operations, API routes, and developer experience benefits.