When to break the monolith
Not every monolith needs splitting. Ours needed it because three teams were stepping on each other — merge conflicts every other day, and the blast radius of any bug was the entire platform. If you have one team and manageable deploy times, keep the monolith.
Choosing the first service to extract
We picked the notification service. It had the clearest boundary: receive an event, format a message, send it. No shared state with the rest of the app, no complex queries. Low risk, high learning value.
RabbitMQ over HTTP
We considered REST calls between services but went with RabbitMQ. The reason: notifications are fire-and-forget. If the notification service is down for 30 seconds, we want messages to queue up, not fail. RabbitMQ gives us that durability for free.
// Producer (monolith)
@Injectable()
export class EventPublisher {
constructor(@Inject('NOTIFICATIONS') private client: ClientProxy) {}
async userRegistered(userId: string, email: string) {
this.client.emit('user.registered', { userId, email });
}
}
// Consumer (notification service)
@Controller()
export class NotificationController {
@EventPattern('user.registered')
async handleUserRegistered(data: { userId: string; email: string }) {
await this.mailer.sendWelcome(data.email);
}
}
Dead letter queues save you at 3am
Messages fail. The consumer crashes, the email provider is down, the payload is malformed. Without a dead letter queue, those messages vanish. We route every failed message to a DLQ and have a simple admin endpoint that lets us inspect and replay them.
The contract problem
The hardest part was not the infrastructure. It was keeping the message contracts in sync between producer and consumer. We created a shared types package published to our private npm registry. Both services import from it. If the shape changes, both sides see the TypeScript error at build time.
Results after 6 months
- Deploy times: 12 min down to 3 min per service
- Notification failures no longer crash the main app
- Three teams deploy independently
- RabbitMQ has handled 14M messages with zero data loss
