I’ve been thinking a lot lately about how modern applications handle complexity at scale. It’s not just about writing code that works—it’s about building systems that can grow and adapt without breaking. That’s why I want to share my experience with event-driven architecture, using NestJS, RabbitMQ, and MongoDB. These tools help create systems that are not only robust but also elegantly responsive to change.
Have you ever considered what happens when your application needs to process thousands of events simultaneously without dropping a single one?
Let’s start with the basics. Event-driven architecture allows different parts of your system to communicate asynchronously through events. This means services can operate independently, reacting to changes without being tightly coupled. In a typical setup, one service publishes an event, and others subscribe to it, each performing their specific tasks. This design promotes scalability and resilience.
Here’s a simple example of defining an event in NestJS:
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly totalAmount: number
) {}
}
Next, we need a reliable way to transmit these events. RabbitMQ excels here as a message broker. It ensures messages are delivered even if some services are temporarily unavailable. Setting it up in NestJS is straightforward:
// app.module.ts
import { RabbitMQModule } from '@nestjs/microservices';
@Module({
imports: [
RabbitMQModule.forRoot({
exchanges: [
{
name: 'orders',
type: 'topic',
},
],
uri: process.env.RABBITMQ_URI,
}),
],
})
export class AppModule {}
Now, where does MongoDB fit in? It serves as our event store, capturing every event that occurs in the system. This historical record is invaluable for auditing, debugging, or even reconstructing application state. Here’s a schema for storing events:
// event.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
@Schema()
export class Event {
@Prop({ required: true })
type: string;
@Prop({ required: true })
aggregateId: string;
@Prop({ type: Object })
data: Record<string, any>;
@Prop({ default: Date.now })
timestamp: Date;
}
export const EventSchema = SchemaFactory.createForClass(Event);
What if a service fails while processing an event? RabbitMQ’s acknowledgment mechanisms allow us to handle such scenarios gracefully. If a message isn’t acknowledged, it can be requeued or moved to a dead-letter queue for later analysis.
Combining these technologies, we can build a system where services like order processing, inventory management, and notifications operate in harmony. Each service focuses on its role, reacting to events as they occur, making the whole system more maintainable and easier to scale.
Here’s how you might publish an event from a service:
// order.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class OrderService {
constructor(private eventEmitter: EventEmitter2) {}
async createOrder(orderData: any) {
// Business logic to create order
const order = await this.saveOrder(orderData);
// Emit event
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, order.customerId, order.totalAmount)
);
return order;
}
}
And subscribing to that event in another service:
// inventory.service.ts
import { OnEvent } from '@nestjs/event-emitter';
export class InventoryService {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent) {
// Update inventory based on the new order
await this.adjustStockLevels(event.orderId, event.items);
}
}
This approach not only separates concerns but also makes it easier to add new functionality. Want to send a confirmation email when an order is placed? Just add a listener for the same event without touching the order service.
I hope this gives you a practical starting point for your own projects. Building with event-driven principles might require a shift in thinking, but the payoff in flexibility and scalability is immense. If you found this useful, I’d love to hear your thoughts—feel free to share, comment, or reach out with questions.