I’ve been thinking a lot about how modern applications need to handle increasing complexity while staying responsive. That’s why event-driven microservices have captured my attention—they offer a way to build systems that are both scalable and resilient. If you’re looking to create applications that can grow and adapt, this approach might be exactly what you need.
Let me show you how to build this architecture using NestJS, RabbitMQ, and TypeScript. We’ll create a system where services communicate through events rather than direct calls, making each component independent and focused.
Why does this matter? Because when services aren’t tightly coupled, you can update one without breaking others. The entire system becomes more flexible and easier to maintain.
Start by setting up your project structure. Create separate directories for each service and a shared library for common code.
mkdir event-driven-microservices
cd event-driven-microservices
mkdir apps libs
cd apps
mkdir user-service order-service notification-service
cd ../libs
mkdir shared
Each service will handle its own domain. The user service manages user data, the order service processes orders, and the notification service sends alerts. They’ll communicate through events published to RabbitMQ.
How do we ensure these services can talk to each other reliably? Let’s set up RabbitMQ using Docker to handle our message queue.
Create a docker-compose.yml file:
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3.12-management
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: password
Run docker-compose up -d
to start RabbitMQ. Now your services have a message broker to communicate through.
In your shared library, define events that services will use. Events are simple data structures that represent something that happened.
export class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly firstName: string,
public readonly lastName: string
) {}
}
When a user registers, the user service publishes this event. Other services can listen and react accordingly.
Now let’s look at the user service. It handles user creation and publishes events when users are created or updated.
@Injectable()
export class UserService {
constructor(private readonly eventPublisher: EventPublisher) {}
async createUser(createUserDto: CreateUserDto) {
const user = await this.userRepository.create(createUserDto);
const event = new UserCreatedEvent(
user.id,
user.email,
user.firstName,
user.lastName
);
this.eventPublisher.publish('user.created', event);
return user;
}
}
The order service listens for these events. When a user is created, it might create a shopping cart for them.
What happens if the order service is down when an event is published? RabbitMQ will keep the message until the service is back online.
@EventHandler(UserCreatedEvent)
export class UserCreatedHandler {
async handle(event: UserCreatedEvent) {
await this.cartService.createCartForUser(event.userId);
}
}
Error handling is crucial in distributed systems. Implement retry logic and dead letter queues for messages that repeatedly fail.
const retryOptions = {
retryAttempts: 3,
retryDelay: 3000,
deadLetterExchange: 'dead_letter'
};
Monitoring is equally important. Use health checks to ensure your services are running properly.
@Get('health')
healthCheck() {
return {
status: 'ok',
timestamp: new Date().toISOString()
};
}
Testing might make you wonder: how do you verify events are being published and handled correctly? Use mock implementations and integration tests.
it('should publish user.created event', async () => {
const publishSpy = jest.spyOn(eventPublisher, 'publish');
await userService.createUser(testUser);
expect(publishSpy).toHaveBeenCalledWith(
'user.created',
expect.any(UserCreatedEvent)
);
});
Deploy your services using Docker. Create a Dockerfile for each service and use docker-compose to manage them together.
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist/ ./dist/
CMD ["node", "dist/main"]
When you run docker-compose up
, all your services will start and connect to RabbitMQ automatically.
This architecture scales well because you can run multiple instances of each service. RabbitMQ will distribute messages evenly across them.
Remember to implement proper logging and tracing. When something goes wrong, you’ll need to track requests across service boundaries.
I encourage you to try building something with this pattern. Start small with two services and expand as you become comfortable with the concepts.
What challenges have you faced with microservices? Share your experiences in the comments below—I’d love to hear how others are solving these problems.
If you found this useful, please like and share it with others who might benefit. Your feedback helps me create better content for our community.