js

Build Production-Ready Event-Driven Microservices with NestJS, NATS, and MongoDB: Complete Developer Guide

Learn to build scalable event-driven microservices using NestJS, NATS messaging, and MongoDB. Master CQRS patterns, saga transactions, and production deployment strategies.

Build Production-Ready Event-Driven Microservices with NestJS, NATS, and MongoDB: Complete Developer Guide

I’ve been working with distributed systems for years, and I keep seeing teams struggle with the same issues. They build microservices that communicate through direct HTTP calls, creating fragile spiderwebs of dependencies. When one service goes down, everything grinds to a halt. That’s why I want to share how we can build more resilient systems using event-driven architecture.

Have you ever wondered what happens when your order processing system can’t reach the notification service? In traditional architectures, the entire transaction fails. But with event-driven systems, the order gets processed, and notifications are sent when the service recovers.

Let me show you how to build production-ready microservices using NestJS, NATS, and MongoDB. This combination has served me well in high-load environments.

First, let’s set up our event bus. NATS provides the messaging backbone that keeps our services connected without direct dependencies.

// event-bus.service.ts
@Injectable()
export class EventBusService {
  private connection: NatsConnection;
  private jc = JSONCodec();

  async publish<T extends BaseEvent>(subject: string, event: T): Promise<void> {
    const js = this.connection.jetstream();
    await js.publish(subject, this.jc.encode(event));
  }

  async subscribe(subject: string, handler: (event: any) => Promise<void>) {
    const js = this.connection.jetstream();
    const subscription = await js.pullSubscribe(subject);
    
    for await (const message of subscription) {
      try {
        await handler(this.jc.decode(message.data));
        message.ack();
      } catch (error) {
        message.nak();
      }
    }
  }
}

Now, imagine a user registration flow. Instead of making synchronous calls to multiple services, we publish an event and let interested services react.

// user.service.ts
@Injectable()
export class UserService {
  constructor(
    private eventBus: EventBusService,
    @InjectModel(User.name) private userModel: Model<User>
  ) {}

  async createUser(createUserDto: CreateUserDto): Promise<User> {
    const user = new this.userModel(createUserDto);
    await user.save();

    await this.eventBus.publish('user.created', {
      id: uuidv4(),
      type: 'UserCreated',
      aggregateId: user.id,
      email: user.email,
      name: user.name,
      timestamp: new Date()
    });

    return user;
  }
}

What happens when the order service needs to know about new users? It simply subscribes to the user.created event without any direct connection to the user service.

// order.service.ts
@Injectable()
export class OrderService {
  constructor(
    private eventBus: EventBusService,
    @InjectModel(Customer.name) private customerModel: Model<Customer>
  ) {}

  async onModuleInit() {
    await this.eventBus.subscribe('user.created', async (event) => {
      const customer = new this.customerModel({
        userId: event.aggregateId,
        email: event.email,
        name: event.name
      });
      await customer.save();
    });
  }
}

But what about complex business transactions that span multiple services? This is where the saga pattern comes in handy. Let me show you how to handle an order creation that involves inventory checks, payment processing, and notifications.

// order-saga.service.ts
@Injectable()
export class OrderSaga {
  private readonly logger = new Logger(OrderSaga.name);

  constructor(
    private eventBus: EventBusService,
    @InjectModel(Order.name) private orderModel: Model<Order>
  ) {}

  async handleOrderCreated(event: OrderCreatedEvent) {
    const order = await this.orderModel.findById(event.aggregateId);
    
    try {
      // Reserve inventory
      await this.eventBus.publish('inventory.reserve.requested', {
        orderId: order.id,
        items: order.items
      });

      order.status = 'INVENTORY_RESERVED';
      await order.save();

    } catch (error) {
      this.logger.error(`Failed to reserve inventory for order ${order.id}`);
      await this.compensateOrder(order.id);
    }
  }
}

Error handling is crucial in distributed systems. We need to ensure our services can recover from failures and maintain data consistency.

// circuit-breaker.service.ts
@Injectable()
export class CircuitBreakerService {
  private failures = 0;
  private lastFailureTime: Date;
  private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime.getTime() > 30000) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is open');
      }
    }

    try {
      const result = await operation();
      this.reset();
      return result;
    } catch (error) {
      this.recordFailure();
      throw error;
    }
  }

  private recordFailure(): void {
    this.failures++;
    this.lastFailureTime = new Date();
    
    if (this.failures >= 5) {
      this.state = 'OPEN';
    }
  }
}

Monitoring is another critical aspect. We need to know what’s happening across our services. I always implement comprehensive logging and metrics collection.

// event-logging.interceptor.ts
@Injectable()
export class EventLoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('EventLogger');

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    
    this.logger.log({
      timestamp: new Date().toISOString(),
      service: 'user-service',
      event: 'HTTP_REQUEST',
      method: request.method,
      path: request.path,
      userId: request.user?.id
    });

    return next.handle();
  }
}

Did you notice how each service maintains its own data? The order service has its customer projection, updated asynchronously from user events. This separation allows each service to optimize its data model for its specific needs.

Here’s how I structure MongoDB collections for event sourcing:

// event-store.schema.ts
@Schema({ collection: 'events' })
export class EventStore extends Document {
  @Prop({ required: true })
  type: string;

  @Prop({ required: true })
  aggregateId: string;

  @Prop({ type: Object, required: true })
  data: any;

  @Prop({ required: true })
  timestamp: Date;

  @Prop({ required: true })
  version: number;
}

export const EventStoreSchema = SchemaFactory.createForClass(EventStore);
EventStoreSchema.index({ aggregateId: 1, version: 1 });

Testing event-driven systems requires a different approach. We need to verify that events are published and handled correctly.

// user.service.spec.ts
describe('UserService', () => {
  let service: UserService;
  let eventBus: EventBusService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: EventBusService,
          useValue: {
            publish: jest.fn().mockResolvedValue(undefined)
          }
        }
      ],
    }).compile();

    service = module.get<UserService>(UserService);
    eventBus = module.get<EventBusService>(EventBusService);
  });

  it('should publish UserCreated event when creating user', async () => {
    const userDto = { email: '[email protected]', name: 'Test User' };
    
    await service.createUser(userDto);

    expect(eventBus.publish).toHaveBeenCalledWith(
      'user.created',
      expect.objectContaining({
        type: 'UserCreated',
        email: '[email protected]'
      })
    );
  });
});

Building event-driven microservices requires shifting your mindset from synchronous request-response to asynchronous event flows. The initial complexity pays off in scalability and resilience.

What patterns have you found most effective in your distributed systems? I’m always curious to learn from others’ experiences.

Remember, the goal isn’t to make everything event-driven overnight. Start with the most critical workflows where loose coupling provides the most value.

I hope this gives you a solid foundation for building your own event-driven systems. If you found this helpful, I’d appreciate it if you could share it with others who might benefit. Feel free to leave comments with your experiences or questions - I learn as much from your feedback as you do from these articles.

Keywords: event-driven microservices, NestJS microservices architecture, NATS messaging system, MongoDB event sourcing, CQRS pattern implementation, distributed transactions saga, microservices error handling, Node.js event-driven architecture, production microservices deployment, scalable microservices design



Similar Posts
Blog Image
How to Build Scalable Event-Driven Microservices with NestJS, RabbitMQ, and Redis: Complete Guide

Learn to build scalable event-driven microservices with NestJS, RabbitMQ, and Redis. Master message queuing, caching, CQRS patterns, and production deployment strategies.

Blog Image
Distributed Rate Limiting with Redis and Node.js: Complete Implementation Guide

Learn to build distributed rate limiting with Redis and Node.js. Complete guide covering token bucket, sliding window algorithms, Express middleware, and production monitoring techniques.

Blog Image
Master Event-Driven Architecture: Complete Node.js EventStore TypeScript Guide with CQRS Implementation

Learn to build event-driven architecture with Node.js, EventStore & TypeScript. Master CQRS, event sourcing, aggregates & projections with hands-on examples.

Blog Image
Building Production-Ready Event-Driven Microservices with NestJS, Redis Streams, and PostgreSQL: Complete Tutorial

Learn to build production-ready event-driven microservices with NestJS, Redis Streams & PostgreSQL. Master reliable messaging, error handling & monitoring.

Blog Image
Complete Guide to Building Real-Time Apps with Svelte and Supabase Integration

Learn how to integrate Svelte with Supabase for rapid web development. Build real-time apps with PostgreSQL, authentication, and reactive UI components seamlessly.