js

Master Event-Driven Architecture: NestJS, Redis Streams & TypeScript Implementation Guide 2024

Learn to build scalable event-driven microservices with NestJS, Redis Streams & TypeScript. Complete guide with error handling, monitoring & production tips.

Master Event-Driven Architecture: NestJS, Redis Streams & TypeScript Implementation Guide 2024

Have you ever watched a large team work together without anyone shouting orders? Each person knows their job, reacts to what they see, and passes information along. That’s the quiet power of an event-driven system. It doesn’t wait to be asked; it reacts when something happens. I keep coming back to this idea because modern applications demand resilience and the ability to grow without becoming a tangled mess of direct calls. Today, I want to show you how to build one using tools I’ve come to trust: NestJS, Redis Streams, and TypeScript.

Think of a simple online store. A customer clicks “buy.” That single action isn’t a single task—it’s a spark. It must reserve stock, charge a card, and send a confirmation. If these steps talk directly to each other, a failure in one can bring down the whole chain. An event-driven design changes this. The “order placed” event becomes a message broadcast into the system. Interested services listen and act independently. The payment service doesn’t need to know about inventory. They just react to the events they care about.

So, how do we set this up? Let’s start with the backbone: Redis Streams. It’s more than a cache; it’s a robust log that stores our events. Messages persist, and multiple consumer groups can read them without losing data. First, we connect.

// redis.service.ts
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisService {
  private publisher: Redis;
  private subscriber: Redis;

  constructor() {
    this.publisher = new Redis({ port: 6379, host: 'localhost' });
    this.subscriber = new Redis({ port: 6379, host: 'localhost' });
  }

  async publishEvent(stream: string, event: object) {
    // Use '*' to let Redis generate a unique ID
    await this.publisher.xadd(stream, '*', 'event', JSON.stringify(event));
  }
}

We need a common language for our events. A base class ensures every event has the structure our system expects.

// base-event.ts
export abstract class BaseEvent {
  public readonly id: string;
  public readonly timestamp: Date;

  constructor(
    public readonly aggregateId: string,
    public readonly type: string,
    public readonly payload: Record<string, any>
  ) {
    this.id = Math.random().toString(36).substring(2, 15);
    this.timestamp = new Date();
  }
}

Now, let’s make something happen. The producer’s job is straightforward: create an event and put it on the correct stream. Here’s an Order Service creating a concrete event.

// order-placed.event.ts
export class OrderPlacedEvent extends BaseEvent {
  constructor(aggregateId: string, payload: { userId: string; items: [] }) {
    super(aggregateId, 'ORDER_PLACED', payload);
  }
}

// order.service.ts
@Injectable()
export class OrderService {
  constructor(private readonly redisService: RedisService) {}

  async placeOrder(orderDetails) {
    // ... create order logic
    const event = new OrderPlacedEvent('order_123', {
      userId: 'user_456',
      items: orderDetails.items,
    });

    // Publish to the orders stream
    await this.redisService.publishEvent('orders.stream', event);
    console.log(`Event ${event.id} published.`);
  }
}

The event is now in the stream, waiting. But what good is a message with no listener? This is where consumers come in. They run continuously, watching the stream for new messages. How do we ensure they handle load and failures gracefully? We use a consumer group. This allows multiple instances of the same service to share the work.

// inventory.consumer.ts
@Injectable()
export class InventoryConsumer {
  private consumerName = `inventory-consumer-${process.pid}`;

  constructor(private readonly redisService: RedisService) {
    this.listenForOrders();
  }

  private async listenForOrders() {
    const stream = 'orders.stream';
    const group = 'inventory-group';

    try {
      // Ensure the group exists
      await this.redisService.createConsumerGroup(stream, group);
    } catch (e) {
      // Group likely already exists
    }

    while (true) {
      // Read new events, blocking until one arrives
      const events = await this.redisService.readFromGroup(
        stream,
        group,
        this.consumerName
      );

      for (const event of events) {
        try {
          const parsedEvent = JSON.parse(event.message.event);
          console.log(`Processing ${parsedEvent.type} for order ${parsedEvent.aggregateId}`);
          // Business logic: reserve stock
          await this.reserveStock(parsedEvent.payload.items);
          // Acknowledge successful processing
          await this.redisService.acknowledgeEvent(stream, group, event.id);
        } catch (error) {
          console.error(`Failed to process event ${event.id}:`, error);
          // Logic for dead-letter queues goes here
        }
      }
    }
  }
}

Notice the acknowledgment step. This tells Redis the message was processed successfully and shouldn’t be delivered again to this group. But what if our processing fails? A robust system needs a plan for errors. One common pattern is a dead-letter queue. Failed events are moved to a separate stream for later inspection or retry.

This approach gives you incredible flexibility. Need to add a promotional email service next month? Just create a new consumer that subscribes to the ORDER_PLACED event. The order service doesn’t need to change at all. This separation is the key to building systems that can evolve without constant rewrites.

What happens when traffic spikes? Since consumers are stateless and independent, you can simply add more instances. Redis will distribute the pending messages across all available consumers in the group. The system scales out horizontally with ease.

Building this requires a shift in thinking. You move from commanding services to emitting facts. It’s about designing the flow of information rather than a chain of commands. The result is software that’s more adaptable and far harder to break.

Give this pattern a try in your next NestJS project. Start small with a single event stream and one consumer. You might be surprised by how clean and responsive your architecture becomes. If you found this walk-through helpful, please like and share it. What challenges have you faced with microservice communication? Let me know in the comments below.

Keywords: NestJS event-driven architecture, Redis Streams microservices, TypeScript event sourcing, distributed systems NestJS, event-driven microservices tutorial, Redis Streams TypeScript, NestJS CQRS implementation, microservices error handling, event sourcing patterns, scalable event architecture



Similar Posts
Blog Image
Build Complete NestJS Authentication System with Refresh Tokens, Prisma, and Redis

Learn to build a complete authentication system with JWT refresh tokens using NestJS, Prisma, and Redis. Includes secure session management, token rotation, and guards.

Blog Image
Build Serverless GraphQL APIs with Apollo Server TypeScript and AWS Lambda Complete Guide

Learn to build scalable serverless GraphQL APIs with Apollo Server, TypeScript & AWS Lambda. Complete guide with authentication, optimization & deployment strategies.

Blog Image
Build Type-Safe Event-Driven Microservices: NestJS, RabbitMQ, and Prisma Complete Tutorial 2024

Learn to build scalable microservices with NestJS, RabbitMQ & Prisma. Master event-driven architecture, type-safe databases & distributed systems. Start building today!

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Apps in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack development. Build powerful React apps with seamless database connectivity and auto-generated APIs.

Blog Image
Build High-Performance GraphQL APIs: Complete TypeScript, Prisma & Apollo Server Development Guide

Learn to build high-performance GraphQL APIs with TypeScript, Prisma & Apollo Server. Master schema-first development, optimization & production deployment.

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

Learn to build scalable distributed rate limiting with Redis & Node.js. Master token bucket, sliding window algorithms, TypeScript middleware & production optimization.