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 Next.js with Prisma ORM: Build Type-Safe Full-Stack Applications in 2024

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

Blog Image
Build Type-Safe GraphQL APIs: Complete NestJS Prisma Code-First Schema Generation Tutorial 2024

Learn to build type-safe GraphQL APIs with NestJS, Prisma & code-first schema generation. Complete tutorial with auth, optimization & deployment tips.

Blog Image
Build High-Performance GraphQL API with NestJS, Prisma and Redis Caching Complete Tutorial

Learn to build a production-ready GraphQL API with NestJS, Prisma, and Redis. Master authentication, caching, DataLoader optimization, and deployment strategies.

Blog Image
How to Build a Professional CLI Tool with TypeScript and Commander.js

Learn how to create a powerful, user-friendly command-line interface using TypeScript, Commander.js, and best UX practices.

Blog Image
Complete Guide to Building Multi-Tenant SaaS Architecture with NestJS, Prisma, and PostgreSQL RLS

Learn to build scalable multi-tenant SaaS with NestJS, Prisma & PostgreSQL RLS. Complete guide with authentication, security & performance tips.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Apps with Modern ORM Database Solutions

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack web applications. Build scalable database-driven apps with seamless frontend-backend unity.