Ever tried coordinating a team where no one talks directly, but somehow everything gets done? That’s the power of event-driven architecture. Today, I want to build something with you. We’re going to connect independent services, letting them chat through events, not direct calls. This approach creates systems that are tough to break and easy to grow. If you stick with me, I’ll show you how to make it work using NestJS, RabbitMQ, and MongoDB. Let’s get started.
First, why choose this path? In a traditional system, services often call each other directly. It works, but it’s fragile. If the payment service is slow, the entire order process grinds to a halt. We can do better. By using events, we let the order service announce “an order was created” and move on. Other services listen and react in their own time. This loose connection is the goal.
So, what are we building? Think of a simple store. A customer places an order. Several things must happen: take payment, check stock, and send a confirmation. We’ll split this into separate services—Order, Payment, Inventory, and Notification. Each will live in its own NestJS application.
Let’s talk about the messenger: RabbitMQ. It’s a message broker. Our services will publish events to it, and RabbitMQ ensures they reach the right listeners. It’s reliable. If a service is down, RabbitMQ holds the message until it comes back. Setting it up is straightforward with Docker.
Here’s a basic docker-compose.yml to run our infrastructure:
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3-management
ports:
- "5672:5672"
- "15672:15672"
mongodb:
image: mongo
ports:
- "27017:27017"
Now, the heart of our system: the events. We need a common language for our services to speak. In a shared library, we define event classes. Here’s what an OrderCreatedEvent might look like:
export class OrderCreatedEvent {
public readonly type = 'order.created';
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly total: number
) {}
}
How do we make a service send this event? In NestJS, we use the built-in microservice client. First, we set up a connection to RabbitMQ in our OrderService module.
// order-service/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
@Module({
imports: [
ClientsModule.register([
{
name: 'EVENT_BUS',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'events_queue',
},
},
]),
],
})
export class AppModule {}
Then, in our OrderService, we inject this client and publish the event after creating an order in MongoDB.
// order-service/src/order.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { OrderCreatedEvent } from '@app/shared/events';
@Injectable()
export class OrderService {
constructor(@Inject('EVENT_BUS') private readonly client: ClientProxy) {}
async createOrder(userId: string, items: any[]) {
// 1. Save order to MongoDB
const newOrder = await this.orderModel.create({ userId, items });
// 2. Publish the event
const event = new OrderCreatedEvent(newOrder.id, userId, newOrder.total);
this.client.emit(event.type, event);
return newOrder;
}
}
See what happened? The order was saved, and an event was fired into the ether. The service doesn’t wait for a response. It just announces the news and continues. But who is listening?
This is where the magic happens. Our PaymentService and InventoryService are both listening for that same order.created event. In NestJS, we create an event handler.
// payment-service/src/listeners/order-created.listener.ts
import { Controller } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
import { OrderCreatedEvent } from '@app/shared/events';
@Controller()
export class PaymentListener {
@EventPattern('order.created')
async handleOrderCreated(@Payload() data: OrderCreatedEvent) {
console.log(`Processing payment for order ${data.orderId}`);
// Logic to charge the user...
}
}
What if the payment fails? We don’t want a stale order sitting there. This is where patterns like Saga come in. A Saga is a sequence of events that manages a business process. If the payment fails, the Saga can trigger a compensating event, like order.cancelled, to undo the reservation in the inventory.
Error handling is critical. In RabbitMQ, we can use a Dead Letter Exchange (DLX). If a message can’t be processed after several tries, it gets moved to a separate queue for manual inspection. This prevents one bad message from clogging the entire system.
Have you considered how you’d track a request as it hops between four different services? This is where observability comes in. Tools like OpenTelemetry can add a unique trace ID to each event, letting you follow the entire journey of an order from creation to delivery in a dashboard.
Testing this setup requires a shift in thinking. You’re not just testing function outputs; you’re verifying that events are published and handled correctly. Use libraries to run a test instance of RabbitMQ and check if the expected messages are on the queue.
When you’re ready to run everything, Docker Compose is your friend. You can define all your services and their dependencies in one file, creating your whole architecture with a single command: docker-compose up.
Building this way might feel complex at first. You are introducing new moving parts—a message broker, separate databases, and event handlers. But the payoff is a system that can withstand the failure of any single component. New features become easier to add; you just create a new service that listens to existing events.
I remember the first time I saw an event-driven system handle a service outage gracefully. The main app kept running, messages queued up, and when the service came back, it processed the backlog without a hitch. It felt robust. It felt right.
What part of this process seems most challenging to you? Is it setting up the message broker, or perhaps designing the events themselves?
Getting all these services to talk without tangling them is the real reward. It’s about creating something where each part can evolve independently. Start small. Build one service that publishes an event and another that listens. You’ll quickly see the pattern and can expand from there.
This is more than a tutorial; it’s a different way to think about building software. If this approach clicks for you, share it with a teammate. Have you built something similar? What hurdles did you face? Let me know in the comments, and if this guide helped you connect the dots, please like and share it