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
Complete Guide to Integrating Next.js with Prisma for Full-Stack TypeScript Applications in 2024

Learn how to integrate Next.js with Prisma for powerful full-stack web apps. Get type-safe database access, seamless API routes, and faster development workflows.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe database operations. Build powerful full-stack apps with seamless data management.

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

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

Blog Image
Build High-Performance GraphQL API: NestJS, TypeORM, Redis Caching Complete Guide 2024

Learn to build scalable GraphQL APIs with NestJS, TypeORM & Redis caching. Master database operations, real-time subscriptions, and performance optimization.

Blog Image
Build High-Performance GraphQL APIs with NestJS, Prisma, and DataLoader: Complete Tutorial

Learn to build scalable GraphQL APIs with NestJS, Prisma & DataLoader. Master authentication, query optimization, real-time subscriptions & production best practices.

Blog Image
Build Full-Stack TypeScript Apps: Complete Next.js and Prisma Integration Guide for Modern Developers

Learn to integrate Next.js with Prisma for type-safe full-stack TypeScript apps. Master database operations, API routes & seamless deployment today.