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 React Server-Side Rendering with Fastify: Setup, Implementation and Performance Optimization

Learn to build fast, SEO-friendly React apps with server-side rendering using Fastify. Complete guide with setup, hydration, routing & deployment tips.

Blog Image
Master GraphQL Performance: Build APIs with Apollo Server and DataLoader Pattern

Learn to build efficient GraphQL APIs with Apollo Server and DataLoader pattern. Solve N+1 query problems, implement advanced caching, and optimize performance. Complete tutorial included.

Blog Image
Production-Ready Event-Driven Architecture: Node.js, TypeScript, RabbitMQ Implementation Guide 2024

Learn to build scalable event-driven architecture with Node.js, TypeScript & RabbitMQ. Master microservices, error handling & production deployment.

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

Master Next.js Prisma integration for type-safe full-stack apps. Learn database setup, API routes, and seamless TypeScript development. Build faster today!

Blog Image
Build Real-Time Web Apps: Complete Guide to Svelte and Socket.IO Integration

Learn how to integrate Svelte with Socket.IO for building fast, real-time web applications with seamless data synchronization and minimal overhead. Start building today!

Blog Image
Build a Distributed Task Queue System with BullMQ Redis and TypeScript Complete Guide

Learn to build scalable task queues with BullMQ, Redis & TypeScript. Master job processing, error handling, monitoring & deployment. Complete tutorial with Express.js integration.